@hivetechs/hive-ai
Version:
Real-time streaming AI consensus platform with HTTP+SSE MCP integration for Claude Code, VS Code, Cursor, and Windsurf - powered by OpenRouter's unified API
274 lines • 9.73 kB
JavaScript
/**
* ASCII Chart Utilities for Terminal Display
*
* Provides simple ASCII-based charts and visualizations for CLI output
*/
/**
* ANSI color codes for terminal output
*/
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
// Foreground colors
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
// Background colors
bgBlack: '\x1b[40m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
bgMagenta: '\x1b[45m',
bgCyan: '\x1b[46m',
bgWhite: '\x1b[47m'
};
/**
* Create a horizontal bar chart
*/
export function createBarChart(data, options = {}) {
const { width = 40, showValues = true, showPercentages = true, title, unit = '' } = options;
// Find max value for scaling
const maxValue = Math.max(...data.map(d => d.value));
if (maxValue === 0)
return 'No data to display';
let output = '';
// Add title if provided
if (title) {
output += `${colors.bright}${title}${colors.reset}\n`;
output += '─'.repeat(width + 30) + '\n';
}
// Generate bars
data.forEach(item => {
const barLength = Math.round((item.value / maxValue) * width);
const barChar = '█';
// Get color
const colorCode = item.color ? colors[item.color] : colors.blue;
// Create bar
const bar = colorCode + barChar.repeat(barLength) + colors.reset;
const emptySpace = ' '.repeat(width - barLength);
// Format label (truncate and pad to consistent width)
const maxLabelLength = 15;
const truncatedLabel = item.label.length > maxLabelLength ?
item.label.substring(0, maxLabelLength - 3) + '...' : item.label;
const label = truncatedLabel.padEnd(maxLabelLength);
// Format value and percentage
let valueStr = '';
if (showValues) {
valueStr = ` ${item.value}${unit}`;
}
if (showPercentages && item.percentage !== undefined) {
valueStr += ` (${item.percentage.toFixed(1)}%)`;
}
output += `${label} ${bar}${emptySpace}${valueStr}\n`;
});
return output;
}
/**
* Create a simple line chart using ASCII
*/
export function createLineChart(data, options = {}) {
const { width = 50, height = 10, title } = options;
if (data.length === 0)
return 'No data to display';
const maxValue = Math.max(...data);
const minValue = Math.min(...data);
const range = maxValue - minValue || 1;
// Create chart grid
const chart = Array(height).fill(null).map(() => Array(width).fill(' '));
// Plot points
data.forEach((value, index) => {
const x = Math.floor((index / (data.length - 1)) * (width - 1));
const y = height - 1 - Math.floor(((value - minValue) / range) * (height - 1));
if (x >= 0 && x < width && y >= 0 && y < height) {
chart[y][x] = '●';
}
});
// Add axes
for (let i = 0; i < height; i++) {
chart[i][0] = '│';
}
for (let i = 0; i < width; i++) {
chart[height - 1][i] = '─';
}
chart[height - 1][0] = '└';
// Convert to string
let output = '';
if (title) {
output += `${colors.bright}${title}${colors.reset}\n\n`;
}
// Add max value label
output += `${maxValue.toFixed(0).padStart(6)} ┤\n`;
// Add chart rows
chart.forEach((row, i) => {
if (i === Math.floor(height / 2)) {
output += `${((maxValue + minValue) / 2).toFixed(0).padStart(6)} ┤`;
}
else if (i < height - 1) {
output += ' │';
}
else {
output += `${minValue.toFixed(0).padStart(6)} ┴`;
}
output += row.join('') + '\n';
});
return output;
}
/**
* Create a progress bar
*/
export function createProgressBar(current, total, options = {}) {
const { width = 30, showPercentage = true, label } = options;
const percentage = total > 0 ? (current / total) * 100 : 0;
const filled = Math.min(Math.max(0, Math.round((percentage / 100) * width)), width);
const empty = Math.max(0, width - filled);
let bar = '[';
bar += colors.green + '█'.repeat(filled) + colors.reset;
bar += '░'.repeat(empty);
bar += ']';
let output = '';
if (label) {
// Truncate label if too long to prevent line overflow
const maxLabelLength = 20; // Reserve space for progress bar and percentage
const truncatedLabel = label.length > maxLabelLength ?
label.substring(0, maxLabelLength - 3) + '...' : label;
output += truncatedLabel.padEnd(maxLabelLength) + ': ';
}
output += bar;
if (showPercentage) {
output += ` ${percentage.toFixed(1)}%`;
}
return output;
}
/**
* Create a gauge chart (semicircle)
*/
export function createGauge(value, max = 100, options = {}) {
const { label, thresholds } = options;
const percentage = (value / max) * 100;
// Determine color based on thresholds
let color = colors.green;
if (thresholds) {
if (percentage >= thresholds.critical) {
color = colors.red;
}
else if (percentage >= thresholds.warning) {
color = colors.yellow;
}
}
// Create gauge visualization
const gaugeWidth = 21;
const filled = Math.round((percentage / 100) * gaugeWidth);
let output = '';
if (label) {
// Truncate label if too long to prevent overflow
const maxLabelLength = gaugeWidth + 8; // Allow some extra space for the gauge frame
const truncatedLabel = label.length > maxLabelLength ?
label.substring(0, maxLabelLength - 3) + '...' : label;
output += `${colors.bright}${truncatedLabel}${colors.reset}\n`;
}
// Top arc
output += ' ╭' + '─'.repeat(gaugeWidth) + '╮\n';
// Gauge bar
output += ' │';
output += color + '█'.repeat(filled) + colors.reset;
output += '░'.repeat(gaugeWidth - filled);
output += '│\n';
// Bottom with value - handle long numbers gracefully
const valueText = `${value}/${max}`;
const maxValueLength = Math.max(6, Math.min(gaugeWidth - 4, valueText.length + 2));
const leftPadding = Math.floor((gaugeWidth - maxValueLength) / 2);
const rightPadding = gaugeWidth - maxValueLength - leftPadding;
output += ' ╰' + '─'.repeat(Math.max(0, leftPadding));
output += ` ${valueText} `;
output += '─'.repeat(Math.max(0, rightPadding)) + '╯\n';
return output;
}
/**
* Create a simple table
*/
export function createTable(headers, rows, options = {}) {
const { alignments = [], colors: rowColors = [] } = options;
// Calculate column widths
const columnWidths = headers.map((header, i) => {
const maxWidth = Math.max(header.length, ...rows.map(row => (row[i] || '').toString().length));
return maxWidth + 2; // Add padding
});
// Create separator
const separator = '├' + columnWidths.map(w => '─'.repeat(w)).join('┼') + '┤\n';
const topBorder = '┌' + columnWidths.map(w => '─'.repeat(w)).join('┬') + '┐\n';
const bottomBorder = '└' + columnWidths.map(w => '─'.repeat(w)).join('┴') + '┘\n';
let output = topBorder;
// Add headers
output += '│';
headers.forEach((header, i) => {
const alignment = alignments[i] || 'left';
const width = columnWidths[i];
output += ' ' + alignText(header, width - 2, alignment) + ' │';
});
output += '\n' + separator;
// Add rows
rows.forEach((row, rowIndex) => {
output += '│';
row.forEach((cell, i) => {
const alignment = alignments[i] || 'left';
const width = columnWidths[i];
const color = rowColors[rowIndex] || '';
const colorCode = color && colors[color] ? colors[color] : '';
const resetCode = colorCode ? colors.reset : '';
output += ' ' + colorCode + alignText(cell.toString(), width - 2, alignment) + resetCode + ' │';
});
output += '\n';
});
output += bottomBorder;
return output;
}
/**
* Helper function to align text
*/
function alignText(text, width, alignment) {
const textLength = text.length;
if (textLength >= width) {
return text.substring(0, width);
}
const padding = width - textLength;
switch (alignment) {
case 'right':
return ' '.repeat(padding) + text;
case 'center':
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
default: // left
return text + ' '.repeat(padding);
}
}
/**
* Create sparkline chart
*/
export function createSparkline(data, width = 20) {
if (data.length === 0)
return '';
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
const sparkChars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
// Sample data if too many points
let sampledData = data;
if (data.length > width) {
const step = data.length / width;
sampledData = Array(width).fill(0).map((_, i) => data[Math.floor(i * step)]);
}
return sampledData.map(value => {
const normalized = (value - min) / range;
const index = Math.round(normalized * (sparkChars.length - 1));
return sparkChars[index];
}).join('');
}
//# sourceMappingURL=ascii-charts.js.map