aura-ai
Version:
AI-powered marketing strategist CLI tool for developers
189 lines (163 loc) • 5.74 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import { aiParser } from './aiQuestionParser.js';
/**
* Convert markdown questions to structured JSON format
*/
export class MarkdownToJsonConverter {
/**
* Parse markdown content and extract questions
* @param {string} content - Markdown content
* @param {boolean} useAI - Whether to use AI for option extraction
* @returns {Object} Structured questions object
*/
async parseMarkdown(content, useAI = true) {
const lines = content.split('\n');
const questions = [];
let questionIndex = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check if line is a question (starts with -)
if (line.trim().startsWith('-') && !line.trim().startsWith('- explainer:')) {
const questionText = line.trim().substring(1).trim();
// Check if question has options in parentheses
const parenMatch = questionText.match(/\(([^)]+)\)/);
let type = 'text';
let options = [];
if (parenMatch) {
const optionsText = parenMatch[1];
options = optionsText.split(',').map(opt => ({
label: opt.trim(),
value: opt.trim().toLowerCase().replace(/\s+/g, '_')
}));
type = 'select';
}
// Remove parentheses from question text for display
const displayQuestion = questionText.replace(/\s*\([^)]+\)\s*/, '').trim();
// Look for explainer on the next line
let explainer = '';
if (i + 1 < lines.length) {
const nextLine = lines[i + 1].trim();
if (nextLine.startsWith('- explainer:')) {
explainer = nextLine.substring('- explainer:'.length).trim();
// Check for multi-line explainer
let j = i + 2;
while (j < lines.length && lines[j].startsWith(' ')) {
explainer += ' ' + lines[j].trim();
j++;
}
}
}
questions.push({
id: `q_${questionIndex + 1}`,
type: type,
question: displayQuestion,
originalQuestion: questionText,
required: true,
options: options,
explainer: explainer,
metadata: {
originalIndex: questionIndex,
category: this.inferCategory(displayQuestion)
}
});
questionIndex++;
}
}
// Enhance with AI if enabled and API key is available
if (useAI && process.env.OPENAI_API_KEY) {
try {
const enhancedQuestions = await aiParser.processQuestions(questions);
return {
version: '1.0.0',
generatedAt: new Date().toISOString(),
totalQuestions: enhancedQuestions.length,
questions: enhancedQuestions,
aiEnhanced: true
};
} catch (error) {
// If AI enhancement fails, continue with basic parsing
}
}
return {
version: '1.0.0',
generatedAt: new Date().toISOString(),
totalQuestions: questions.length,
questions: questions,
aiEnhanced: false
};
}
/**
* Infer category from question text
* @param {string} questionText - The question text
* @returns {string} Category name
*/
inferCategory(questionText) {
const text = questionText.toLowerCase();
if (text.includes('customer') || text.includes('audience') || text.includes('user')) {
return 'audience';
}
if (text.includes('competitor') || text.includes('alternative')) {
return 'competition';
}
if (text.includes('channel') || text.includes('reach')) {
return 'marketing';
}
if (text.includes('problem') || text.includes('solution') || text.includes('feature')) {
return 'product';
}
return 'general';
}
/**
* Convert markdown file to JSON
* @param {string} inputPath - Path to markdown file
* @param {string} outputPath - Path to output JSON file
* @param {boolean} useAI - Whether to use AI for option extraction
*/
async convertFile(inputPath, outputPath, useAI = false) {
try {
// Read markdown file
const content = await fs.readFile(inputPath, 'utf-8');
// Parse to JSON (now async)
const jsonData = await this.parseMarkdown(content, useAI);
// Add source file info
jsonData.source = {
file: path.basename(inputPath),
path: inputPath,
lastModified: (await fs.stat(inputPath)).mtime.toISOString()
};
// Write JSON file
await fs.writeFile(
outputPath,
JSON.stringify(jsonData, null, 2),
'utf-8'
);
// Conversion successful - no console output to avoid breaking the UI
return jsonData;
} catch (error) {
// Error occurred but don't log to avoid breaking the UI
throw error;
}
}
/**
* Watch markdown file for changes and auto-convert
* @param {string} inputPath - Path to markdown file
* @param {string} outputPath - Path to output JSON file
*/
async watchAndConvert(inputPath, outputPath) {
// Initial conversion
await this.convertFile(inputPath, outputPath);
// Watch for changes
const { watch } = await import('fs');
const watcher = watch(inputPath, async (eventType) => {
if (eventType === 'change') {
// File changed, regenerating JSON silently
await this.convertFile(inputPath, outputPath);
}
});
// Watching for changes silently
return watcher;
}
}
// Export a singleton instance
export const converter = new MarkdownToJsonConverter();