feat: enhance ideation routes with event handling and new suggestion feature

- Updated the ideation routes to include an EventEmitter for better event management.
- Added a new endpoint to handle adding suggestions to the board, ensuring consistent category mapping.
- Modified existing routes to emit events for idea creation, update, and deletion, improving frontend notifications.
- Refactored the convert and create idea handlers to utilize the new event system.
- Removed static guided prompts data in favor of dynamic fetching from the backend API.
This commit is contained in:
webdevcody
2026-01-04 00:38:01 -05:00
parent 5c95d6d58e
commit ac92725a6c
20 changed files with 442 additions and 538 deletions

View File

@@ -218,7 +218,7 @@ app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));
app.use('/api/mcp', createMCPRoutes(mcpTestService));
app.use('/api/pipeline', createPipelineRoutes(pipelineService));
app.use('/api/ideation', createIdeationRoutes(ideationService, featureLoader));
app.use('/api/ideation', createIdeationRoutes(events, ideationService, featureLoader));
// Create HTTP server
const server = createServer(app);

View File

@@ -3,6 +3,7 @@
*/
import { Router } from 'express';
import type { EventEmitter } from '../../lib/events.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import type { IdeationService } from '../../services/ideation-service.js';
import type { FeatureLoader } from '../../services/feature-loader.js';
@@ -19,10 +20,12 @@ import { createIdeasUpdateHandler } from './routes/ideas-update.js';
import { createIdeasDeleteHandler } from './routes/ideas-delete.js';
import { createAnalyzeHandler, createGetAnalysisHandler } from './routes/analyze.js';
import { createConvertHandler } from './routes/convert.js';
import { createAddSuggestionHandler } from './routes/add-suggestion.js';
import { createPromptsHandler, createPromptsByCategoryHandler } from './routes/prompts.js';
import { createSuggestionsGenerateHandler } from './routes/suggestions-generate.js';
export function createIdeationRoutes(
events: EventEmitter,
ideationService: IdeationService,
featureLoader: FeatureLoader
): Router {
@@ -35,7 +38,7 @@ export function createIdeationRoutes(
createSessionStartHandler(ideationService)
);
router.post('/session/message', createSessionMessageHandler(ideationService));
router.post('/session/stop', createSessionStopHandler(ideationService));
router.post('/session/stop', createSessionStopHandler(events, ideationService));
router.post(
'/session/get',
validatePathParams('projectPath'),
@@ -51,7 +54,7 @@ export function createIdeationRoutes(
router.post(
'/ideas/create',
validatePathParams('projectPath'),
createIdeasCreateHandler(ideationService)
createIdeasCreateHandler(events, ideationService)
);
router.post(
'/ideas/get',
@@ -61,12 +64,12 @@ export function createIdeationRoutes(
router.post(
'/ideas/update',
validatePathParams('projectPath'),
createIdeasUpdateHandler(ideationService)
createIdeasUpdateHandler(events, ideationService)
);
router.post(
'/ideas/delete',
validatePathParams('projectPath'),
createIdeasDeleteHandler(ideationService)
createIdeasDeleteHandler(events, ideationService)
);
// Project analysis
@@ -81,7 +84,14 @@ export function createIdeationRoutes(
router.post(
'/convert',
validatePathParams('projectPath'),
createConvertHandler(ideationService, featureLoader)
createConvertHandler(events, ideationService, featureLoader)
);
// Add suggestion to board as a feature
router.post(
'/add-suggestion',
validatePathParams('projectPath'),
createAddSuggestionHandler(ideationService, featureLoader)
);
// Guided prompts (no validation needed - static data)

View File

@@ -0,0 +1,70 @@
/**
* POST /add-suggestion - Add an analysis suggestion to the board as a feature
*
* This endpoint converts an AnalysisSuggestion to a Feature using the
* IdeationService's mapIdeaCategoryToFeatureCategory for consistent category mapping.
* This ensures a single source of truth for the conversion logic.
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { FeatureLoader } from '../../../services/feature-loader.js';
import type { AnalysisSuggestion } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createAddSuggestionHandler(
ideationService: IdeationService,
featureLoader: FeatureLoader
) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, suggestion } = req.body as {
projectPath: string;
suggestion: AnalysisSuggestion;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!suggestion) {
res.status(400).json({ success: false, error: 'suggestion is required' });
return;
}
if (!suggestion.title) {
res.status(400).json({ success: false, error: 'suggestion.title is required' });
return;
}
if (!suggestion.category) {
res.status(400).json({ success: false, error: 'suggestion.category is required' });
return;
}
// Build description with rationale if provided
const description = suggestion.rationale
? `${suggestion.description}\n\n**Rationale:** ${suggestion.rationale}`
: suggestion.description;
// Use the service's category mapping for consistency
const featureCategory = ideationService.mapSuggestionCategoryToFeatureCategory(
suggestion.category
);
// Create the feature
const feature = await featureLoader.create(projectPath, {
title: suggestion.title,
description,
category: featureCategory,
status: 'backlog',
});
res.json({ success: true, featureId: feature.id });
} catch (error) {
logError(error, 'Add suggestion to board failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -3,12 +3,14 @@
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { FeatureLoader } from '../../../services/feature-loader.js';
import type { ConvertToFeatureOptions } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createConvertHandler(
events: EventEmitter,
ideationService: IdeationService,
featureLoader: FeatureLoader
) {
@@ -49,8 +51,22 @@ export function createConvertHandler(
// Delete the idea unless keepIdea is explicitly true
if (!keepIdea) {
await ideationService.deleteIdea(projectPath, ideaId);
// Emit idea deleted event
events.emit('ideation:idea-deleted', {
projectPath,
ideaId,
});
}
// Emit idea converted event to notify frontend
events.emit('ideation:idea-converted', {
projectPath,
ideaId,
featureId: feature.id,
keepIdea: !!keepIdea,
});
// Return featureId as expected by the frontend API interface
res.json({ success: true, featureId: feature.id });
} catch (error) {

View File

@@ -3,11 +3,12 @@
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { CreateIdeaInput } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasCreateHandler(ideationService: IdeationService) {
export function createIdeasCreateHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, idea } = req.body as {
@@ -34,6 +35,13 @@ export function createIdeasCreateHandler(ideationService: IdeationService) {
}
const created = await ideationService.createIdea(projectPath, idea);
// Emit idea created event for frontend notification
events.emit('ideation:idea-created', {
projectPath,
idea: created,
});
res.json({ success: true, idea: created });
} catch (error) {
logError(error, 'Create idea failed');

View File

@@ -3,10 +3,11 @@
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasDeleteHandler(ideationService: IdeationService) {
export function createIdeasDeleteHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, ideaId } = req.body as {
@@ -25,6 +26,13 @@ export function createIdeasDeleteHandler(ideationService: IdeationService) {
}
await ideationService.deleteIdea(projectPath, ideaId);
// Emit idea deleted event for frontend notification
events.emit('ideation:idea-deleted', {
projectPath,
ideaId,
});
res.json({ success: true });
} catch (error) {
logError(error, 'Delete idea failed');

View File

@@ -3,11 +3,12 @@
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { UpdateIdeaInput } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasUpdateHandler(ideationService: IdeationService) {
export function createIdeasUpdateHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, ideaId, updates } = req.body as {
@@ -37,6 +38,13 @@ export function createIdeasUpdateHandler(ideationService: IdeationService) {
return;
}
// Emit idea updated event for frontend notification
events.emit('ideation:idea-updated', {
projectPath,
ideaId,
idea,
});
res.json({ success: true, idea });
} catch (error) {
logError(error, 'Update idea failed');

View File

@@ -26,7 +26,7 @@ export function createPromptsByCategoryHandler(ideationService: IdeationService)
try {
const { category } = req.params as { category: string };
const validCategories: IdeaCategory[] = ['feature', 'ux-ui', 'dx', 'growth', 'technical'];
const validCategories = ideationService.getPromptCategories().map((c) => c.id);
if (!validCategories.includes(category as IdeaCategory)) {
res.status(400).json({ success: false, error: 'Invalid category' });
return;

View File

@@ -3,13 +3,17 @@
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createSessionStopHandler(ideationService: IdeationService) {
export function createSessionStopHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { sessionId } = req.body as { sessionId: string };
const { sessionId, projectPath } = req.body as {
sessionId: string;
projectPath?: string;
};
if (!sessionId) {
res.status(400).json({ success: false, error: 'sessionId is required' });
@@ -17,6 +21,15 @@ export function createSessionStopHandler(ideationService: IdeationService) {
}
await ideationService.stopSession(sessionId);
// Emit session stopped event for frontend notification
// Note: The service also emits 'ideation:session-ended' internally,
// but we emit here as well for route-level consistency with other routes
events.emit('ideation:session-ended', {
sessionId,
projectPath,
});
res.json({ success: true });
} catch (error) {
logError(error, 'Stop session failed');

View File

@@ -5,6 +5,7 @@
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('ideation:suggestions-generate');
@@ -45,10 +46,10 @@ export function createSuggestionsGenerateHandler(ideationService: IdeationServic
suggestions,
});
} catch (error) {
logger.error('Failed to generate suggestions:', error);
logError(error, 'Failed to generate suggestions');
res.status(500).json({
success: false,
error: (error as Error).message,
error: getErrorMessage(error),
});
}
};

View File

@@ -39,6 +39,7 @@ import { ProviderFactory } from '../providers/provider-factory.js';
import type { SettingsService } from './settings-service.js';
import type { FeatureLoader } from './feature-loader.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { resolveModelString } from '@automaker/model-resolver';
const logger = createLogger('IdeationService');
@@ -200,20 +201,22 @@ export class IdeationService {
existingWorkContext
);
// Resolve model alias to canonical identifier
const modelId = resolveModelString(options?.model ?? 'sonnet');
// Create SDK options
const sdkOptions = createChatOptions({
cwd: projectPath,
model: options?.model || 'sonnet',
model: modelId,
systemPrompt,
abortController: activeSession.abortController!,
});
const effectiveModel = sdkOptions.model!;
const provider = ProviderFactory.getProviderForModel(effectiveModel);
const provider = ProviderFactory.getProviderForModel(modelId);
const executeOptions: ExecuteOptions = {
prompt: message,
model: effectiveModel,
model: modelId,
cwd: projectPath,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: 1, // Single turn for ideation
@@ -645,20 +648,22 @@ export class IdeationService {
existingWorkContext
);
// Resolve model alias to canonical identifier
const modelId = resolveModelString('sonnet');
// Create SDK options
const sdkOptions = createChatOptions({
cwd: projectPath,
model: 'sonnet',
model: modelId,
systemPrompt,
abortController: new AbortController(),
});
const effectiveModel = sdkOptions.model!;
const provider = ProviderFactory.getProviderForModel(effectiveModel);
const provider = ProviderFactory.getProviderForModel(modelId);
const executeOptions: ExecuteOptions = {
prompt: prompt.prompt,
model: effectiveModel,
model: modelId,
cwd: projectPath,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: 1,
@@ -892,6 +897,30 @@ ${contextSection}${existingWorkSection}`;
icon: 'Cpu',
description: 'Architecture and infrastructure',
},
{
id: 'security',
name: 'Security',
icon: 'Shield',
description: 'Security improvements and vulnerability fixes',
},
{
id: 'performance',
name: 'Performance',
icon: 'Gauge',
description: 'Performance optimization and speed improvements',
},
{
id: 'accessibility',
name: 'Accessibility',
icon: 'Accessibility',
description: 'Accessibility features and inclusive design',
},
{
id: 'analytics',
name: 'Analytics',
icon: 'BarChart',
description: 'Analytics, monitoring, and insights features',
},
];
}
@@ -905,7 +934,8 @@ ${contextSection}${existingWorkSection}`;
/**
* Get all guided prompts
* NOTE: Keep in sync with apps/ui/src/components/views/ideation-view/data/guided-prompts.ts
* This is the single source of truth for guided prompts data.
* Frontend fetches this data via /api/ideation/prompts endpoint.
*/
getAllPrompts(): IdeationPrompt[] {
return [
@@ -1629,7 +1659,20 @@ Focus on practical, implementable suggestions that would genuinely improve the p
return `${summary}. Found ${suggestions.length} improvement opportunities${highPriority > 0 ? ` (${highPriority} high priority)` : ''}.`;
}
/**
* Map idea category to feature category
* Used internally for idea-to-feature conversion
*/
private mapIdeaCategoryToFeatureCategory(category: IdeaCategory): string {
return this.mapSuggestionCategoryToFeatureCategory(category);
}
/**
* Map suggestion/idea category to feature category
* This is the single source of truth for category mapping.
* Used by both idea-to-feature conversion and suggestion-to-feature conversion.
*/
mapSuggestionCategoryToFeatureCategory(category: IdeaCategory): string {
const mapping: Record<IdeaCategory, string> = {
feature: 'ui',
'ux-ui': 'enhancement',