pdm-ai
Version:
PDM-AI - Transform customer feedback into structured product insights using the Jobs-to-be-Done (JTBD) methodology
151 lines (128 loc) • 6.17 kB
JavaScript
/**
* Visualization command for PDM-AI
* Generates visual representations of JTBDs and scenarios
*/
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import * as visualization from '../utils/visualization/index.js';
/**
* Generate a default output filename based on the input file and format
* @param {string} inputFile - Path to input file
* @param {string} format - Output format (mermaid, csv)
* @returns {string} Default output filename
*/
function generateDefaultOutputFilename(inputFile, format) {
const baseName = path.basename(inputFile, path.extname(inputFile));
const extension = format === 'mermaid' ? 'md' : format;
return path.join(process.cwd(), '.pdm', 'outputs', 'visualizations', `${baseName}-viz.${extension}`);
}
/**
* Execute the visualize command
* @param {string} input - Input JSON file with JTBDs and scenarios
* @param {Object} options - Command options
*/
async function execute(input, options = {}) {
try {
// Set default options
options.format = options.format || 'mermaid';
options.perspective = options.perspective || 'jtbd';
options.maxNodes = options.maxNodes || 100;
const verbose = options.verbose;
if (verbose) {
console.log(chalk.blue(`Generating visualization from: ${input}`));
console.log(chalk.blue(`Using format: ${options.format}, perspective: ${options.perspective}`));
}
// Validate input file exists
const inputPath = path.resolve(process.cwd(), input);
if (!fs.existsSync(inputPath)) {
console.error(chalk.red(`Error: Input file does not exist: ${inputPath}`));
process.exit(1);
}
// Determine output path
const outputPath = options.output
? path.resolve(process.cwd(), options.output)
: generateDefaultOutputFilename(inputPath, options.format);
if (verbose) {
console.log(chalk.blue(`Output will be saved to: ${outputPath}`));
}
// Read input file
const inputData = await fs.readJson(inputPath);
let dataToVisualize = inputData;
// Check if the file has the expected structure
// For JTBD perspective, we need both JTBDs and scenarios
if (options.perspective === 'jtbd') {
// Check if we have JTBD data
if (!inputData.jtbds || !Array.isArray(inputData.jtbds) || inputData.jtbds.length === 0) {
console.error(chalk.red(`Error: Input file doesn't contain valid JTBD data for JTBD perspective`));
process.exit(1);
}
// Check if we already have scenario data in the same file (new format)
if (!inputData.scenarios || !Array.isArray(inputData.scenarios) || inputData.scenarios.length === 0) {
console.error(chalk.red(`Error: Input file doesn't contain scenario data, which is required for JTBD perspective`));
console.error(chalk.red(`Use the updated 'pdm jtbd' command to generate files that include both JTBDs and scenarios`));
process.exit(1);
}
} else if (options.perspective === 'persona') {
// For persona perspective, we only need scenarios
if (!inputData.scenarios || !Array.isArray(inputData.scenarios) || inputData.scenarios.length === 0) {
console.error(chalk.red(`Error: Input file doesn't contain valid scenario data required for persona perspective`));
process.exit(1);
}
}
// Prepare visualization options
const visualizationOptions = {
format: options.format,
view: options.perspective, // Map perspective to view for backward compatibility
filter: options.filter,
maxNodes: parseInt(options.maxNodes, 10),
includeFullStatements: true, // Always include full statements for better context
outputPath: outputPath // Pass the output path for CSV generation
};
console.log(chalk.blue(`Building ${options.perspective}-centric visualization...`));
// Generate visualization
const result = await visualization.generateVisualization(dataToVisualize, visualizationOptions);
// Ensure output directory exists
await fs.ensureDir(path.dirname(outputPath));
// Handle result based on format
if (options.format === 'mermaid') {
// For Mermaid format, write a single file
await fs.writeFile(outputPath, result.content);
console.log(chalk.green(`✓ Visualization generated successfully!`));
console.log(chalk.green(` - Output file: ${outputPath}`));
if (verbose) {
console.log(chalk.green(` - Format: ${options.format}`));
console.log(chalk.green(` - Perspective: ${options.perspective}`));
console.log(chalk.green(` - Nodes rendered: ${result.stats.nodeCount}`));
console.log(chalk.green(` - Edges rendered: ${result.stats.edgeCount}`));
console.log(chalk.yellow(`\nTo view the Mermaid diagram, paste the contents of the output file`));
console.log(chalk.yellow(`into a Markdown viewer that supports Mermaid syntax.`));
}
}
else if (options.format === 'csv') {
// For CSV format, multiple files are generated
console.log(chalk.green(`✓ CSV files generated successfully!`));
console.log(chalk.green(` - Files generated: ${result.stats.fileCount}`));
if (verbose) {
console.log(chalk.green(` - Format: ${options.format}`));
console.log(chalk.green(` - Perspective: ${options.perspective}`));
console.log(chalk.green(` - JTBDs processed: ${result.stats.jtbdCount}`));
console.log(chalk.green(` - Scenarios included: ${result.stats.scenarioCount}`));
// List all generated CSV files
console.log(chalk.yellow(`\nGenerated CSV files:`));
result.files.forEach(file => {
console.log(chalk.yellow(` - ${file.path}`));
});
console.log(chalk.yellow(`\nThese CSV files can be imported into Figma or Miro.`));
}
}
return outputPath;
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
if (options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
export { execute };