@vfarcic/dot-ai
Version:
AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
144 lines (143 loc) • 5.49 kB
JavaScript
;
/**
* Shared Visualization Utilities (PRD #320)
*
* Common utilities for visualization support across all MCP tools.
* Provides session metadata interfaces, URL generation, and prompt selection.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TOOL_SESSION_PREFIXES = exports.VISUALIZATION_PREFIX = void 0;
exports.getPromptForTool = getPromptForTool;
exports.getVisualizationUrl = getVisualizationUrl;
exports.extractPrefixFromSessionId = extractPrefixFromSessionId;
exports.getToolNameFromPrefix = getToolNameFromPrefix;
exports.parseVisualizationResponse = parseVisualizationResponse;
/**
* Visualization mode prefix - when present in intent, return visualization data directly
* Used by tools to detect when caller wants visualization output instead of summary
*/
exports.VISUALIZATION_PREFIX = '[visualization]';
/**
* Get the prompt file name for visualization
* All tools use the unified visualize.md template (PRD #320)
*
* @param toolName - Name of the tool (unused, kept for API compatibility)
* @returns Prompt file name (without .md extension)
*/
function getPromptForTool(_toolName) {
return 'visualize';
}
/**
* Get visualization URL if WEB_UI_BASE_URL is configured
* Feature toggle - only returns URL when env var is set
*
* @param sessionIds - Single session ID or array of session IDs to include in URL
* @returns Visualization URL or undefined if not configured
*/
function getVisualizationUrl(sessionIds) {
const baseUrl = process.env.WEB_UI_BASE_URL;
if (!baseUrl) {
return undefined;
}
// Remove trailing slash if present, then append /v/{sessionId(s)}
const normalizedBaseUrl = baseUrl.replace(/\/$/, '');
// Join multiple session IDs with + separator
const sessionPath = Array.isArray(sessionIds) ? sessionIds.join('+') : sessionIds;
return `${normalizedBaseUrl}/v/${sessionPath}`;
}
/**
* Extract the session prefix from a session ID
* Session IDs are formatted: {prefix}-{timestamp}-{uuid}
*
* @param sessionId - Full session ID (e.g., 'qry-1704067200000-a1b2c3d4')
* @returns Session prefix (e.g., 'qry')
*/
function extractPrefixFromSessionId(sessionId) {
const parts = sessionId.split('-');
// Return first part as prefix, default to 'qry' if invalid
return parts[0] || 'qry';
}
/**
* Session prefixes used by each tool
* Useful for documentation and validation
*/
exports.TOOL_SESSION_PREFIXES = {
query: 'qry',
recommend: 'sol', // "solution" - recommend tool provides solutions
remediate: 'rem',
operate: 'opr',
version: 'ver',
projectSetup: 'prj',
};
/**
* Get the tool name from a session prefix
* Reverse lookup from prefix to tool name
*
* @param prefix - Session prefix (e.g., 'qry')
* @returns Tool name or undefined if not recognized
*/
function getToolNameFromPrefix(prefix) {
for (const [tool, toolPrefix] of Object.entries(exports.TOOL_SESSION_PREFIXES)) {
if (toolPrefix === prefix) {
return tool;
}
}
return undefined;
}
/**
* Parse AI response into VisualizationResponse
* Extracts JSON from AI response, validates structure, normalizes insights
*
* @param aiResponse - Raw AI response string
* @param toolsUsed - Optional array of tools used during generation
* @returns Parsed VisualizationResponse
* @throws Error if parsing or validation fails
*/
function parseVisualizationResponse(aiResponse, toolsUsed) {
// Extract JSON from response - it may have text before/after the JSON block
let jsonContent = aiResponse.trim();
// Find JSON block in markdown code fence
const jsonBlockMatch = jsonContent.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonBlockMatch) {
jsonContent = jsonBlockMatch[1].trim();
}
else if (!jsonContent.startsWith('{')) {
// Try to find raw JSON object if no code fence
const jsonStart = jsonContent.indexOf('{');
const jsonEnd = jsonContent.lastIndexOf('}');
if (jsonStart !== -1 && jsonEnd !== -1) {
jsonContent = jsonContent.substring(jsonStart, jsonEnd + 1);
}
}
const parsed = JSON.parse(jsonContent);
// Validate required fields
if (!parsed.title || !Array.isArray(parsed.visualizations) || !Array.isArray(parsed.insights)) {
throw new Error('Invalid visualization response structure');
}
// Validate each visualization has required fields
for (const viz of parsed.visualizations) {
if (!viz.id || !viz.label || !viz.type || viz.content === undefined) {
throw new Error(`Invalid visualization: missing required fields in ${JSON.stringify(viz)}`);
}
if (!['mermaid', 'cards', 'code', 'table', 'diff', 'bar-chart'].includes(viz.type)) {
throw new Error(`Invalid visualization type: ${viz.type}`);
}
}
// Normalize insights to strings if they are objects
const normalizedInsights = parsed.insights.map((insight) => {
if (typeof insight === 'string') {
return insight;
}
// Convert object insights to string format
if (insight.title && insight.description) {
const severity = insight.severity ? ` [${insight.severity}]` : '';
return `${insight.title}${severity}: ${insight.description}`;
}
return String(insight);
});
return {
...parsed,
insights: normalizedInsights,
...(toolsUsed && { toolsUsed })
};
}