Skip to content
Open

Ai #342

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 72 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,100 @@ Interactive Onboarding Tutorials in most cases consist of a code editor on the l

## Authoring a Tutorial

### Create Tutorial Folder
### Setup

To create a new tutorial, first clone the repo:
Clone the repo and install dependencies from the root:

```bash
git clone git@github.com:Vonage-Community/tutorial-interactive_tutorials.git
cd tutorial-interactive_tutorials
npm install
```

Change directory into the tutorials folder, then install the dependencies:
### Create a New Tutorial

Run the create command from the repo root:

```bash
cd tutorials
npm install
npm run create-tutorial
```

Create a copy of the starter tutorial, giving the target folder your tutorial name. Then change directory into your tutorial folder:
You will be prompted for a tutorial name. You can also pass the name directly:

```bash
cp -r 00_Starter-Tutorial product_name-language-topic
cd product_name-language-topic
npm run create-tutorial -- messages_api-node-whatsapp
```

> Keep to the tutorial name taxonomy, `product_name-language-topic`. For example a Messages API WhatsApp tutorial in Node.js would be `messages_api-node-whatsapp`.

### Create the Tutorial Content
This will scaffold a new tutorial folder under `tutorials/` with the correct structure and placeholder content.

### Edit an Existing Tutorial

Run the edit command from the repo root:

```bash
npm run edit-tutorial
```

The tutorials are static websites built with [Astro](https://astro.build). Start the development server:
This will show an interactive list of tutorials to choose from. You can also pass the name directly:

```bash
npm run edit-tutorial -- messages_api-node-whatsapp
```

The command will:
1. Open the tutorial folder in VS Code
2. Start the Astro dev server
3. Open `http://localhost:4321` in your browser automatically

Press **Ctrl+C** in the terminal to stop the dev server.

### Create the Tutorial Content

The tutorials are static websites built with [Astro](https://astro.build). Use `npm run edit-tutorial` from the repo root to open your tutorial and start the dev server automatically, or start it manually from your tutorial's folder:

```bash
cd tutorials/your-tutorial-name
npm run dev
```

This will start your site on your local machine. You can now edit the tutorial content in the `src` folder. Tutorials support markdown, markdoc, and HTML. Once you are done, add a small synopsis to the README.md file in your tutorial's folder.
You can now edit the tutorial content in the `src` folder. Tutorials support markdown, markdoc, and HTML. Once you are done, add a small synopsis to the README.md file in your tutorial's folder.

### Using AI to Draft Tutorial Content

Every tutorial includes authoring instructions that guide AI tools to produce consistent output — correct file naming, structure, frontmatter, and Markdoc components — without any per-developer setup.

#### VS Code Copilot Chat / GitHub Copilot CLI

`.github/copilot-instructions.md` is automatically loaded for every Copilot conversation in the tutorial workspace. Just describe what you want and Copilot will follow the conventions.

To create a new step using the guided prompt:
1. Open Copilot Chat in VS Code
2. Run the command **`Chat: Use Prompt...`**
3. Select **`new-step`**
4. Answer the questions (step number, name, description, code)

Copilot will create the correctly named and formatted file in `src/content/docs/`.

#### OpenCode / OpenAI Codex CLI

`AGENTS.md` in the tutorial root is automatically read by OpenCode and Codex CLI. The same naming and structure rules apply — just describe the step you want.

#### Vim, Neovim, or any other AI tool

Include `AGENTS.md` as context for your AI tool. Most plugins support a `#file:` reference or a system prompt file. For example, with avante.nvim:

```lua
-- In your Neovim config or a project .nvim.lua:
require('avante').setup({ system_prompt = vim.fn.readfile('AGENTS.md') })
```

Or simply paste the contents of `AGENTS.md` into your tool's system prompt / context window.

#### Reference example

`src/content/docs/02-step-template.md` is an annotated example of a complete, well-formed tutorial step. Use it as a starting point or reference when writing manually.

### Create the Tutorial Config

Expand Down
186 changes: 186 additions & 0 deletions create-tutorial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import fs from 'fs/promises';
import path from 'path';
import readline from 'readline/promises';
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TUTORIALS_DIR = path.join(__dirname, 'tutorials');
const STARTER_DIR = path.join(TUTORIALS_DIR, '00_Starter-Tutorial');

// Folders/files to skip when copying the starter template
const SKIP = new Set(['.astro', 'node_modules', 'vonage-toolbar', 'dist']);

const NAME_PATTERN = /^[a-z0-9]+(?:[_-][a-z0-9]+)*$/;
const NAME_EXAMPLE = 'product_name-language-topic (e.g. messages_api-node-sms)';

function toTitle(name) {
return name
.replace(/[_-]+/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}

async function copyDir(src, dest) {
await fs.mkdir(dest, { recursive: true });
for (const entry of await fs.readdir(src, { withFileTypes: true })) {
if (SKIP.has(entry.name)) continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDir(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}

async function replaceInFile(filePath, replacements) {
let content = await fs.readFile(filePath, 'utf8');
for (const [from, to] of replacements) {
content = content.replaceAll(from, to);
}
await fs.writeFile(filePath, content);
}

async function writeTutorialConfig(dest, config) {
await fs.writeFile(
path.join(dest, 'tutorial-config.json'),
JSON.stringify(config, null, 2)
);
}

async function main() {
console.log('\n🎓 Vonage Tutorial Creator\n');

// Single readline instance — collect ALL answers before any async scaffolding
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

const ask = (q) => rl.question(q);
const yesNo = async (question, defaultYes = true) => {
const hint = defaultYes ? '(Y/n)' : '(y/N)';
const answer = (await ask(`${question} ${hint}: `)).trim().toLowerCase();
if (answer === '') return defaultYes;
return answer === 'y' || answer === 'yes';
};
const collectList = async (prompt) => {
const items = [];
while (true) {
const value = (await ask(prompt)).trim();
if (!value) break;
items.push(value);
}
return items;
};

let name, title, dest, config, startEditing;

try {
// --- Tutorial name ---
name = process.argv[2];
if (name) {
if (!NAME_PATTERN.test(name)) {
console.error(`\n❌ Invalid name "${name}". Use lowercase letters, numbers, hyphens, and underscores.`);
console.error(` Format: ${NAME_EXAMPLE}\n`);
process.exit(1);
}
} else {
while (true) {
name = (await ask(`Tutorial name (${NAME_EXAMPLE}): `)).trim();
if (!name) { console.log(' Name cannot be empty.'); continue; }
if (!NAME_PATTERN.test(name)) { console.log(' Use only lowercase letters, numbers, hyphens, and underscores.'); continue; }
break;
}
}

title = toTitle(name);
dest = path.join(TUTORIALS_DIR, name);

// Guard: already exists
try {
await fs.access(dest);
console.error(`\n❌ "${name}" already exists at tutorials/${name}\n`);
process.exit(1);
} catch { /* expected */ }

// --- Toolbar config: steps 1–4 (ask before scaffolding so stdin stays open) ---
console.log(`\n📋 Tutorial: ${name} (${title})`);
console.log('⚙️ Answer a few questions to configure it:\n');

// Step 1: Panels
const panels = [];
if (await yesNo('Step 1: Does this tutorial need a Terminal panel?')) panels.push('terminal');
if (await yesNo('Step 1: Does this tutorial need a Preview Browser panel?', false)) panels.push('browser');

// Step 2: External repository
console.log('');
const repository = (await ask('Step 2: External repository URL? (press Enter to skip): ')).trim();

// Step 3: Starter files
console.log('\nStep 3: Starter files (pre-populated files with scaffolding code).');
console.log(' Press Enter with no input when done.');
const starterFiles = await collectList(' Starter file: ');

// Step 4: Files to open
console.log('\nStep 4: Files to open in the editor when the tutorial starts.');
console.log(' Press Enter with no input when done.');
const openFiles = await collectList(' File to open: ');

config = { files: [], openFiles, panels, repository, starterFiles, capabilities: [], version: '0.0.1', filename: name };

// Ask about editing before closing readline
console.log('');
startEditing = await yesNo('🚀 Start editing when ready?');
} finally {
rl.close();
}

// --- Scaffold (after all input collected) ---
console.log(`\n📂 Creating tutorials/${name}...\n`);

await copyDir(STARTER_DIR, dest);
console.log('✅ Copied starter template');

await replaceInFile(path.join(dest, 'astro.config.mjs'), [['Vonage Onboarding', title]]);
console.log('✅ Updated astro.config.mjs');

await replaceInFile(path.join(dest, 'src/content/docs/index.mdx'), [
['Starter Tutorial', title],
['Get started building a tutorial.', `Get started with ${title}.`],
['Starting a tutorial!', `Starting ${title}!`],
]);
console.log('✅ Updated index.mdx');

await replaceInFile(path.join(dest, 'src/content/docs/01-welcome.md'), [
['Starter Tutorial', title],
['This is a paragraph about what the tutorial contains.', `Welcome to the ${title} tutorial.`],
]);
console.log('✅ Updated 01-welcome.md');

await replaceInFile(path.join(dest, 'README.md'), [
['Starter Onboarding Tutorial', title],
['This is a starter tutorial.', `Tutorial: ${title}`],
]);
console.log('✅ Updated README.md');

await replaceInFile(path.join(dest, 'package.json'), [
['"vonage-interactive-tutorial"', `"${name}"`],
]);
console.log('✅ Updated package.json');

await writeTutorialConfig(dest, config);
console.log('✅ Created tutorial-config.json');

console.log('\n✅ Tutorial created successfully!\n');

if (startEditing) {
spawn('node', ['edit-tutorial.js', name], { cwd: __dirname, stdio: 'inherit' });
} else {
console.log(`Next steps:\n\n npm run edit-tutorial -- ${name}\n`);
}
}

main().catch(err => {
console.error('\n❌', err.message);
process.exit(1);
});

Loading
Loading