mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: implement ideation feature for brainstorming and idea management
- Introduced a new IdeationService to manage brainstorming sessions, including idea creation, analysis, and conversion to features. - Added RESTful API routes for ideation, including session management, idea CRUD operations, and suggestion generation. - Created UI components for the ideation dashboard, prompt selection, and category grid to enhance user experience. - Integrated keyboard shortcuts and navigation for the ideation feature, improving accessibility and workflow. - Updated state management with Zustand to handle ideation-specific data and actions. - Added necessary types and paths for ideation functionality, ensuring type safety and clarity in the codebase.
This commit is contained in:
1
.claude/.gitignore
vendored
Normal file
1
.claude/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hans/
|
||||
@@ -58,6 +58,8 @@ import { createMCPRoutes } from './routes/mcp/index.js';
|
||||
import { MCPTestService } from './services/mcp-test-service.js';
|
||||
import { createPipelineRoutes } from './routes/pipeline/index.js';
|
||||
import { pipelineService } from './services/pipeline-service.js';
|
||||
import { createIdeationRoutes } from './routes/ideation/index.js';
|
||||
import { IdeationService } from './services/ideation-service.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
@@ -162,6 +164,7 @@ const featureLoader = new FeatureLoader();
|
||||
const autoModeService = new AutoModeService(events, settingsService);
|
||||
const claudeUsageService = new ClaudeUsageService();
|
||||
const mcpTestService = new MCPTestService(settingsService);
|
||||
const ideationService = new IdeationService(events, settingsService, featureLoader);
|
||||
|
||||
// Initialize services
|
||||
(async () => {
|
||||
@@ -215,6 +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));
|
||||
|
||||
// Create HTTP server
|
||||
const server = createServer(app);
|
||||
|
||||
@@ -89,7 +89,7 @@ export class ClaudeProvider extends BaseProvider {
|
||||
...(allowedTools && shouldRestrictTools && { allowedTools }),
|
||||
...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }),
|
||||
// When MCP servers are configured and auto-approve is enabled, use bypassPermissions
|
||||
permissionMode: shouldBypassPermissions ? 'bypassPermissions' : 'default',
|
||||
permissionMode: shouldBypassPermissions ? 'bypassPermissions' : 'acceptEdits',
|
||||
// Required when using bypassPermissions mode
|
||||
...(shouldBypassPermissions && { allowDangerouslySkipPermissions: true }),
|
||||
abortController,
|
||||
|
||||
12
apps/server/src/routes/ideation/common.ts
Normal file
12
apps/server/src/routes/ideation/common.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Common utilities for ideation routes
|
||||
*/
|
||||
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||
|
||||
const logger = createLogger('Ideation');
|
||||
|
||||
// Re-export shared utilities
|
||||
export { getErrorMessageShared as getErrorMessage };
|
||||
export const logError = createLogError(logger);
|
||||
99
apps/server/src/routes/ideation/index.ts
Normal file
99
apps/server/src/routes/ideation/index.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Ideation routes - HTTP API for brainstorming and idea management
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||
import type { IdeationService } from '../../services/ideation-service.js';
|
||||
import type { FeatureLoader } from '../../services/feature-loader.js';
|
||||
|
||||
// Route handlers
|
||||
import { createSessionStartHandler } from './routes/session-start.js';
|
||||
import { createSessionMessageHandler } from './routes/session-message.js';
|
||||
import { createSessionStopHandler } from './routes/session-stop.js';
|
||||
import { createSessionGetHandler } from './routes/session-get.js';
|
||||
import { createIdeasListHandler } from './routes/ideas-list.js';
|
||||
import { createIdeasCreateHandler } from './routes/ideas-create.js';
|
||||
import { createIdeasGetHandler } from './routes/ideas-get.js';
|
||||
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 { createPromptsHandler, createPromptsByCategoryHandler } from './routes/prompts.js';
|
||||
import { createSuggestionsGenerateHandler } from './routes/suggestions-generate.js';
|
||||
|
||||
export function createIdeationRoutes(
|
||||
ideationService: IdeationService,
|
||||
featureLoader: FeatureLoader
|
||||
): Router {
|
||||
const router = Router();
|
||||
|
||||
// Session management
|
||||
router.post(
|
||||
'/session/start',
|
||||
validatePathParams('projectPath'),
|
||||
createSessionStartHandler(ideationService)
|
||||
);
|
||||
router.post('/session/message', createSessionMessageHandler(ideationService));
|
||||
router.post('/session/stop', createSessionStopHandler(ideationService));
|
||||
router.post(
|
||||
'/session/get',
|
||||
validatePathParams('projectPath'),
|
||||
createSessionGetHandler(ideationService)
|
||||
);
|
||||
|
||||
// Ideas CRUD
|
||||
router.post(
|
||||
'/ideas/list',
|
||||
validatePathParams('projectPath'),
|
||||
createIdeasListHandler(ideationService)
|
||||
);
|
||||
router.post(
|
||||
'/ideas/create',
|
||||
validatePathParams('projectPath'),
|
||||
createIdeasCreateHandler(ideationService)
|
||||
);
|
||||
router.post(
|
||||
'/ideas/get',
|
||||
validatePathParams('projectPath'),
|
||||
createIdeasGetHandler(ideationService)
|
||||
);
|
||||
router.post(
|
||||
'/ideas/update',
|
||||
validatePathParams('projectPath'),
|
||||
createIdeasUpdateHandler(ideationService)
|
||||
);
|
||||
router.post(
|
||||
'/ideas/delete',
|
||||
validatePathParams('projectPath'),
|
||||
createIdeasDeleteHandler(ideationService)
|
||||
);
|
||||
|
||||
// Project analysis
|
||||
router.post('/analyze', validatePathParams('projectPath'), createAnalyzeHandler(ideationService));
|
||||
router.post(
|
||||
'/analysis',
|
||||
validatePathParams('projectPath'),
|
||||
createGetAnalysisHandler(ideationService)
|
||||
);
|
||||
|
||||
// Convert to feature
|
||||
router.post(
|
||||
'/convert',
|
||||
validatePathParams('projectPath'),
|
||||
createConvertHandler(ideationService, featureLoader)
|
||||
);
|
||||
|
||||
// Guided prompts (no validation needed - static data)
|
||||
router.get('/prompts', createPromptsHandler(ideationService));
|
||||
router.get('/prompts/:category', createPromptsByCategoryHandler(ideationService));
|
||||
|
||||
// Generate suggestions (structured output)
|
||||
router.post(
|
||||
'/suggestions/generate',
|
||||
validatePathParams('projectPath'),
|
||||
createSuggestionsGenerateHandler(ideationService)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
49
apps/server/src/routes/ideation/routes/analyze.ts
Normal file
49
apps/server/src/routes/ideation/routes/analyze.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* POST /analyze - Analyze project and generate suggestions
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createAnalyzeHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Start analysis - results come via WebSocket events
|
||||
ideationService.analyzeProject(projectPath).catch((error) => {
|
||||
logError(error, 'Analyze project failed (async)');
|
||||
});
|
||||
|
||||
res.json({ success: true, message: 'Analysis started' });
|
||||
} catch (error) {
|
||||
logError(error, 'Analyze project failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetAnalysisHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ideationService.getCachedAnalysis(projectPath);
|
||||
res.json({ success: true, result });
|
||||
} catch (error) {
|
||||
logError(error, 'Get analysis failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
61
apps/server/src/routes/ideation/routes/convert.ts
Normal file
61
apps/server/src/routes/ideation/routes/convert.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* POST /convert - Convert an idea to a feature
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
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(
|
||||
ideationService: IdeationService,
|
||||
featureLoader: FeatureLoader
|
||||
) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, ideaId, keepIdea, column, dependencies, tags } = req.body as {
|
||||
projectPath: string;
|
||||
ideaId: string;
|
||||
} & ConvertToFeatureOptions;
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ideaId) {
|
||||
res.status(400).json({ success: false, error: 'ideaId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert idea to feature structure
|
||||
const featureData = await ideationService.convertToFeature(projectPath, ideaId);
|
||||
|
||||
// Apply any options from the request
|
||||
if (column) {
|
||||
featureData.status = column;
|
||||
}
|
||||
if (dependencies && dependencies.length > 0) {
|
||||
featureData.dependencies = dependencies;
|
||||
}
|
||||
if (tags && tags.length > 0) {
|
||||
featureData.tags = tags;
|
||||
}
|
||||
|
||||
// Create the feature using FeatureLoader
|
||||
const feature = await featureLoader.create(projectPath, featureData);
|
||||
|
||||
// Delete the idea unless keepIdea is explicitly true
|
||||
if (!keepIdea) {
|
||||
await ideationService.deleteIdea(projectPath, ideaId);
|
||||
}
|
||||
|
||||
// Return featureId as expected by the frontend API interface
|
||||
res.json({ success: true, featureId: feature.id });
|
||||
} catch (error) {
|
||||
logError(error, 'Convert to feature failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
43
apps/server/src/routes/ideation/routes/ideas-create.ts
Normal file
43
apps/server/src/routes/ideation/routes/ideas-create.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* POST /ideas/create - Create a new idea
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
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) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, idea } = req.body as {
|
||||
projectPath: string;
|
||||
idea: CreateIdeaInput;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!idea) {
|
||||
res.status(400).json({ success: false, error: 'idea is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!idea.title || !idea.description || !idea.category) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'idea must have title, description, and category',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await ideationService.createIdea(projectPath, idea);
|
||||
res.json({ success: true, idea: created });
|
||||
} catch (error) {
|
||||
logError(error, 'Create idea failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
34
apps/server/src/routes/ideation/routes/ideas-delete.ts
Normal file
34
apps/server/src/routes/ideation/routes/ideas-delete.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* POST /ideas/delete - Delete an idea
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createIdeasDeleteHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, ideaId } = req.body as {
|
||||
projectPath: string;
|
||||
ideaId: string;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ideaId) {
|
||||
res.status(400).json({ success: false, error: 'ideaId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
await ideationService.deleteIdea(projectPath, ideaId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, 'Delete idea failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
39
apps/server/src/routes/ideation/routes/ideas-get.ts
Normal file
39
apps/server/src/routes/ideation/routes/ideas-get.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* POST /ideas/get - Get a single idea
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createIdeasGetHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, ideaId } = req.body as {
|
||||
projectPath: string;
|
||||
ideaId: string;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ideaId) {
|
||||
res.status(400).json({ success: false, error: 'ideaId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const idea = await ideationService.getIdea(projectPath, ideaId);
|
||||
if (!idea) {
|
||||
res.status(404).json({ success: false, error: 'Idea not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, idea });
|
||||
} catch (error) {
|
||||
logError(error, 'Get idea failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
26
apps/server/src/routes/ideation/routes/ideas-list.ts
Normal file
26
apps/server/src/routes/ideation/routes/ideas-list.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* POST /ideas/list - List all ideas for a project
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createIdeasListHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const ideas = await ideationService.getIdeas(projectPath);
|
||||
res.json({ success: true, ideas });
|
||||
} catch (error) {
|
||||
logError(error, 'List ideas failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
46
apps/server/src/routes/ideation/routes/ideas-update.ts
Normal file
46
apps/server/src/routes/ideation/routes/ideas-update.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* POST /ideas/update - Update an idea
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
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) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, ideaId, updates } = req.body as {
|
||||
projectPath: string;
|
||||
ideaId: string;
|
||||
updates: UpdateIdeaInput;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ideaId) {
|
||||
res.status(400).json({ success: false, error: 'ideaId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updates) {
|
||||
res.status(400).json({ success: false, error: 'updates is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const idea = await ideationService.updateIdea(projectPath, ideaId, updates);
|
||||
if (!idea) {
|
||||
res.status(404).json({ success: false, error: 'Idea not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, idea });
|
||||
} catch (error) {
|
||||
logError(error, 'Update idea failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
42
apps/server/src/routes/ideation/routes/prompts.ts
Normal file
42
apps/server/src/routes/ideation/routes/prompts.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* GET /prompts - Get all guided prompts
|
||||
* GET /prompts/:category - Get prompts for a specific category
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import type { IdeaCategory } from '@automaker/types';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createPromptsHandler(ideationService: IdeationService) {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const prompts = ideationService.getAllPrompts();
|
||||
const categories = ideationService.getPromptCategories();
|
||||
res.json({ success: true, prompts, categories });
|
||||
} catch (error) {
|
||||
logError(error, 'Get prompts failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createPromptsByCategoryHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { category } = req.params as { category: string };
|
||||
|
||||
const validCategories: IdeaCategory[] = ['feature', 'ux-ui', 'dx', 'growth', 'technical'];
|
||||
if (!validCategories.includes(category as IdeaCategory)) {
|
||||
res.status(400).json({ success: false, error: 'Invalid category' });
|
||||
return;
|
||||
}
|
||||
|
||||
const prompts = ideationService.getPromptsByCategory(category as IdeaCategory);
|
||||
res.json({ success: true, prompts });
|
||||
} catch (error) {
|
||||
logError(error, 'Get prompts by category failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
45
apps/server/src/routes/ideation/routes/session-get.ts
Normal file
45
apps/server/src/routes/ideation/routes/session-get.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* POST /session/get - Get an ideation session with messages
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createSessionGetHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, sessionId } = req.body as {
|
||||
projectPath: string;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await ideationService.getSession(projectPath, sessionId);
|
||||
if (!session) {
|
||||
res.status(404).json({ success: false, error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const isRunning = ideationService.isSessionRunning(sessionId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
session: { ...session, isRunning },
|
||||
messages: session.messages,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Get session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
40
apps/server/src/routes/ideation/routes/session-message.ts
Normal file
40
apps/server/src/routes/ideation/routes/session-message.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* POST /session/message - Send a message in an ideation session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import type { SendMessageOptions } from '@automaker/types';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createSessionMessageHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { sessionId, message, options } = req.body as {
|
||||
sessionId: string;
|
||||
message: string;
|
||||
options?: SendMessageOptions;
|
||||
};
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
res.status(400).json({ success: false, error: 'message is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// This is async but we don't await - responses come via WebSocket
|
||||
ideationService.sendMessage(sessionId, message, options).catch((error) => {
|
||||
logError(error, 'Send message failed (async)');
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, 'Send message failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
30
apps/server/src/routes/ideation/routes/session-start.ts
Normal file
30
apps/server/src/routes/ideation/routes/session-start.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* POST /session/start - Start a new ideation session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import type { StartSessionOptions } from '@automaker/types';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createSessionStartHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, options } = req.body as {
|
||||
projectPath: string;
|
||||
options?: StartSessionOptions;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await ideationService.startSession(projectPath, options);
|
||||
res.json({ success: true, session });
|
||||
} catch (error) {
|
||||
logError(error, 'Start session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
26
apps/server/src/routes/ideation/routes/session-stop.ts
Normal file
26
apps/server/src/routes/ideation/routes/session-stop.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* POST /session/stop - Stop an ideation session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createSessionStopHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { sessionId } = req.body as { sessionId: string };
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
await ideationService.stopSession(sessionId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, 'Stop session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Generate suggestions route - Returns structured AI suggestions for a prompt
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IdeationService } from '../../../services/ideation-service.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('ideation:suggestions-generate');
|
||||
|
||||
export function createSuggestionsGenerateHandler(ideationService: IdeationService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, promptId, category, count } = req.body;
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!promptId) {
|
||||
res.status(400).json({ success: false, error: 'promptId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!category) {
|
||||
res.status(400).json({ success: false, error: 'category is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to 10 suggestions, allow 1-20
|
||||
const suggestionCount = Math.min(Math.max(count || 10, 1), 20);
|
||||
|
||||
logger.info(`Generating ${suggestionCount} suggestions for prompt: ${promptId}`);
|
||||
|
||||
const suggestions = await ideationService.generateSuggestions(
|
||||
projectPath,
|
||||
promptId,
|
||||
category,
|
||||
suggestionCount
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
suggestions,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate suggestions:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
1679
apps/server/src/services/ideation-service.ts
Normal file
1679
apps/server/src/services/ideation-service.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import {
|
||||
CircleDot,
|
||||
GitPullRequest,
|
||||
Zap,
|
||||
Lightbulb,
|
||||
} from 'lucide-react';
|
||||
import type { NavSection, NavItem } from '../types';
|
||||
import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
|
||||
@@ -30,6 +31,9 @@ interface UseNavigationProps {
|
||||
agent: string;
|
||||
terminal: string;
|
||||
settings: string;
|
||||
ideation: string;
|
||||
githubIssues: string;
|
||||
githubPrs: string;
|
||||
};
|
||||
hideSpecEditor: boolean;
|
||||
hideContext: boolean;
|
||||
@@ -92,6 +96,12 @@ export function useNavigation({
|
||||
// Build navigation sections
|
||||
const navSections: NavSection[] = useMemo(() => {
|
||||
const allToolsItems: NavItem[] = [
|
||||
{
|
||||
id: 'ideation',
|
||||
label: 'Ideation',
|
||||
icon: Lightbulb,
|
||||
shortcut: shortcuts.ideation,
|
||||
},
|
||||
{
|
||||
id: 'spec',
|
||||
label: 'Spec Editor',
|
||||
@@ -172,12 +182,14 @@ export function useNavigation({
|
||||
id: 'github-issues',
|
||||
label: 'Issues',
|
||||
icon: CircleDot,
|
||||
shortcut: shortcuts.githubIssues,
|
||||
count: unviewedValidationsCount,
|
||||
},
|
||||
{
|
||||
id: 'github-prs',
|
||||
label: 'Pull Requests',
|
||||
icon: GitPullRequest,
|
||||
shortcut: shortcuts.githubPrs,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -90,6 +90,9 @@ const SHORTCUT_LABELS: Record<keyof KeyboardShortcuts, string> = {
|
||||
settings: 'Settings',
|
||||
profiles: 'AI Profiles',
|
||||
terminal: 'Terminal',
|
||||
ideation: 'Ideation',
|
||||
githubIssues: 'GitHub Issues',
|
||||
githubPrs: 'Pull Requests',
|
||||
toggleSidebar: 'Toggle Sidebar',
|
||||
addFeature: 'Add Feature',
|
||||
addContextFile: 'Add Context File',
|
||||
@@ -115,6 +118,9 @@ const SHORTCUT_CATEGORIES: Record<keyof KeyboardShortcuts, 'navigation' | 'ui' |
|
||||
settings: 'navigation',
|
||||
profiles: 'navigation',
|
||||
terminal: 'navigation',
|
||||
ideation: 'navigation',
|
||||
githubIssues: 'navigation',
|
||||
githubPrs: 'navigation',
|
||||
toggleSidebar: 'ui',
|
||||
addFeature: 'action',
|
||||
addContextFile: 'action',
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
ArchiveAllVerifiedDialog,
|
||||
DeleteCompletedFeatureDialog,
|
||||
EditFeatureDialog,
|
||||
FeatureSuggestionsDialog,
|
||||
FollowUpDialog,
|
||||
PlanApprovalDialog,
|
||||
} from './board-view/dialogs';
|
||||
@@ -56,7 +55,6 @@ import {
|
||||
useBoardBackground,
|
||||
useBoardPersistence,
|
||||
useFollowUpState,
|
||||
useSuggestionsState,
|
||||
} from './board-view/hooks';
|
||||
|
||||
// Stable empty array to avoid infinite loop in selector
|
||||
@@ -153,19 +151,6 @@ export function BoardView() {
|
||||
handleFollowUpDialogChange,
|
||||
} = useFollowUpState();
|
||||
|
||||
// Suggestions state hook
|
||||
const {
|
||||
showSuggestionsDialog,
|
||||
suggestionsCount,
|
||||
featureSuggestions,
|
||||
isGeneratingSuggestions,
|
||||
setShowSuggestionsDialog,
|
||||
setSuggestionsCount,
|
||||
setFeatureSuggestions,
|
||||
setIsGeneratingSuggestions,
|
||||
updateSuggestions,
|
||||
closeSuggestionsDialog,
|
||||
} = useSuggestionsState();
|
||||
// Search filter for Kanban cards
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
// Plan approval loading state
|
||||
@@ -200,9 +185,6 @@ export function BoardView() {
|
||||
currentProject,
|
||||
specCreatingForProject,
|
||||
setSpecCreatingForProject,
|
||||
setSuggestionsCount,
|
||||
setFeatureSuggestions,
|
||||
setIsGeneratingSuggestions,
|
||||
checkContextExists,
|
||||
features: hookFeatures,
|
||||
isLoading,
|
||||
@@ -1119,8 +1101,6 @@ export function BoardView() {
|
||||
runningAutoTasks={runningAutoTasks}
|
||||
shortcuts={shortcuts}
|
||||
onStartNextFeatures={handleStartNextFeatures}
|
||||
onShowSuggestions={() => setShowSuggestionsDialog(true)}
|
||||
suggestionsCount={suggestionsCount}
|
||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||
pipelineConfig={
|
||||
currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null
|
||||
@@ -1269,17 +1249,6 @@ export function BoardView() {
|
||||
isMaximized={isMaximized}
|
||||
/>
|
||||
|
||||
{/* Feature Suggestions Dialog */}
|
||||
<FeatureSuggestionsDialog
|
||||
open={showSuggestionsDialog}
|
||||
onClose={closeSuggestionsDialog}
|
||||
projectPath={currentProject.path}
|
||||
suggestions={featureSuggestions}
|
||||
setSuggestions={updateSuggestions}
|
||||
isGenerating={isGeneratingSuggestions}
|
||||
setIsGenerating={setIsGeneratingSuggestions}
|
||||
/>
|
||||
|
||||
{/* Backlog Plan Dialog */}
|
||||
<BacklogPlanDialog
|
||||
open={showPlanDialog}
|
||||
|
||||
@@ -1,575 +0,0 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Loader2,
|
||||
Lightbulb,
|
||||
Download,
|
||||
StopCircle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
Shield,
|
||||
Zap,
|
||||
List,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getElectronAPI,
|
||||
FeatureSuggestion,
|
||||
SuggestionsEvent,
|
||||
SuggestionType,
|
||||
} from '@/lib/electron';
|
||||
import { useAppStore, Feature } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
import { LogViewer } from '@/components/ui/log-viewer';
|
||||
|
||||
interface FeatureSuggestionsDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
projectPath: string;
|
||||
// Props to persist state across dialog open/close
|
||||
suggestions: FeatureSuggestion[];
|
||||
setSuggestions: (suggestions: FeatureSuggestion[]) => void;
|
||||
isGenerating: boolean;
|
||||
setIsGenerating: (generating: boolean) => void;
|
||||
}
|
||||
|
||||
// Configuration for each suggestion type
|
||||
const suggestionTypeConfig: Record<
|
||||
SuggestionType,
|
||||
{
|
||||
label: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
description: string;
|
||||
color: string;
|
||||
}
|
||||
> = {
|
||||
features: {
|
||||
label: 'Feature Suggestions',
|
||||
icon: Lightbulb,
|
||||
description: 'Discover missing features and improvements',
|
||||
color: 'text-yellow-500',
|
||||
},
|
||||
refactoring: {
|
||||
label: 'Refactoring Suggestions',
|
||||
icon: RefreshCw,
|
||||
description: 'Find code smells and refactoring opportunities',
|
||||
color: 'text-blue-500',
|
||||
},
|
||||
security: {
|
||||
label: 'Security Suggestions',
|
||||
icon: Shield,
|
||||
description: 'Identify security vulnerabilities and issues',
|
||||
color: 'text-red-500',
|
||||
},
|
||||
performance: {
|
||||
label: 'Performance Suggestions',
|
||||
icon: Zap,
|
||||
description: 'Discover performance bottlenecks and optimizations',
|
||||
color: 'text-green-500',
|
||||
},
|
||||
};
|
||||
|
||||
export function FeatureSuggestionsDialog({
|
||||
open,
|
||||
onClose,
|
||||
projectPath,
|
||||
suggestions,
|
||||
setSuggestions,
|
||||
isGenerating,
|
||||
setIsGenerating,
|
||||
}: FeatureSuggestionsDialogProps) {
|
||||
const [progress, setProgress] = useState<string[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [currentSuggestionType, setCurrentSuggestionType] = useState<SuggestionType | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'parsed' | 'raw'>('parsed');
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const autoScrollRef = useRef(true);
|
||||
|
||||
const { features, setFeatures } = useAppStore();
|
||||
|
||||
// Initialize selectedIds when suggestions change
|
||||
useEffect(() => {
|
||||
if (suggestions.length > 0 && selectedIds.size === 0) {
|
||||
setSelectedIds(new Set(suggestions.map((s) => s.id)));
|
||||
}
|
||||
}, [suggestions, selectedIds.size]);
|
||||
|
||||
// Auto-scroll progress when new content arrives
|
||||
useEffect(() => {
|
||||
if (autoScrollRef.current && scrollRef.current && isGenerating) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [progress, isGenerating]);
|
||||
|
||||
// Listen for suggestion events when dialog is open
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.suggestions) return;
|
||||
|
||||
const unsubscribe = api.suggestions.onEvent((event: SuggestionsEvent) => {
|
||||
if (event.type === 'suggestions_progress') {
|
||||
setProgress((prev) => [...prev, event.content || '']);
|
||||
} else if (event.type === 'suggestions_tool') {
|
||||
const toolName = event.tool || 'Unknown Tool';
|
||||
const toolInput = event.input ? JSON.stringify(event.input, null, 2) : '';
|
||||
const formattedTool = `\n🔧 Tool: ${toolName}\n${toolInput ? `Input: ${toolInput}\n` : ''}`;
|
||||
setProgress((prev) => [...prev, formattedTool]);
|
||||
} else if (event.type === 'suggestions_complete') {
|
||||
setIsGenerating(false);
|
||||
if (event.suggestions && event.suggestions.length > 0) {
|
||||
setSuggestions(event.suggestions);
|
||||
// Select all by default
|
||||
setSelectedIds(new Set(event.suggestions.map((s) => s.id)));
|
||||
const typeLabel = currentSuggestionType
|
||||
? suggestionTypeConfig[currentSuggestionType].label.toLowerCase()
|
||||
: 'suggestions';
|
||||
toast.success(`Generated ${event.suggestions.length} ${typeLabel}!`);
|
||||
} else {
|
||||
toast.info('No suggestions generated. Try again.');
|
||||
}
|
||||
} else if (event.type === 'suggestions_error') {
|
||||
setIsGenerating(false);
|
||||
toast.error(`Error: ${event.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [open, setSuggestions, setIsGenerating, currentSuggestionType]);
|
||||
|
||||
// Start generating suggestions for a specific type
|
||||
const handleGenerate = useCallback(
|
||||
async (suggestionType: SuggestionType) => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.suggestions) {
|
||||
toast.error('Suggestions API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
setProgress([]);
|
||||
setSuggestions([]);
|
||||
setSelectedIds(new Set());
|
||||
setCurrentSuggestionType(suggestionType);
|
||||
|
||||
try {
|
||||
const result = await api.suggestions.generate(projectPath, suggestionType);
|
||||
if (!result.success) {
|
||||
toast.error(result.error || 'Failed to start generation');
|
||||
setIsGenerating(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate suggestions:', error);
|
||||
toast.error('Failed to start generation');
|
||||
setIsGenerating(false);
|
||||
}
|
||||
},
|
||||
[projectPath, setIsGenerating, setSuggestions]
|
||||
);
|
||||
|
||||
// Stop generating
|
||||
const handleStop = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.suggestions) return;
|
||||
|
||||
try {
|
||||
await api.suggestions.stop();
|
||||
setIsGenerating(false);
|
||||
toast.info('Generation stopped');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop generation:', error);
|
||||
}
|
||||
}, [setIsGenerating]);
|
||||
|
||||
// Toggle suggestion selection
|
||||
const toggleSelection = useCallback((id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Toggle expand/collapse for a suggestion
|
||||
const toggleExpanded = useCallback((id: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Select/deselect all
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
if (selectedIds.size === suggestions.length) {
|
||||
setSelectedIds(new Set());
|
||||
} else {
|
||||
setSelectedIds(new Set(suggestions.map((s) => s.id)));
|
||||
}
|
||||
}, [selectedIds.size, suggestions]);
|
||||
|
||||
// Import selected suggestions as features
|
||||
const handleImport = useCallback(async () => {
|
||||
if (selectedIds.size === 0) {
|
||||
toast.warning('No suggestions selected');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsImporting(true);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const selectedSuggestions = suggestions.filter((s) => selectedIds.has(s.id));
|
||||
|
||||
// Create new features from selected suggestions
|
||||
const newFeatures: Feature[] = selectedSuggestions.map((s) => ({
|
||||
id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
category: s.category,
|
||||
description: s.description,
|
||||
steps: [], // Required empty steps array for new features
|
||||
status: 'backlog' as const,
|
||||
skipTests: true, // As specified, testing mode true
|
||||
priority: s.priority, // Preserve priority from suggestion
|
||||
}));
|
||||
|
||||
// Create each new feature using the features API
|
||||
if (api.features) {
|
||||
for (const feature of newFeatures) {
|
||||
await api.features.create(projectPath, feature);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with existing features for store update
|
||||
const updatedFeatures = [...features, ...newFeatures];
|
||||
|
||||
// Update store
|
||||
setFeatures(updatedFeatures);
|
||||
|
||||
toast.success(`Imported ${newFeatures.length} features to backlog!`);
|
||||
|
||||
// Clear suggestions after importing
|
||||
setSuggestions([]);
|
||||
setSelectedIds(new Set());
|
||||
setProgress([]);
|
||||
setCurrentSuggestionType(null);
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to import features:', error);
|
||||
toast.error('Failed to import features');
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
}, [selectedIds, suggestions, features, setFeatures, setSuggestions, projectPath, onClose]);
|
||||
|
||||
// Handle scroll to detect if user scrolled up
|
||||
const handleScroll = () => {
|
||||
if (!scrollRef.current) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||
autoScrollRef.current = isAtBottom;
|
||||
};
|
||||
|
||||
// Go back to type selection
|
||||
const handleBackToSelection = useCallback(() => {
|
||||
setSuggestions([]);
|
||||
setSelectedIds(new Set());
|
||||
setProgress([]);
|
||||
setCurrentSuggestionType(null);
|
||||
}, [setSuggestions]);
|
||||
|
||||
const hasStarted = isGenerating || progress.length > 0 || suggestions.length > 0;
|
||||
const hasSuggestions = suggestions.length > 0;
|
||||
const currentConfig = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType] : null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className="w-[70vw] max-w-[70vw] max-h-[85vh] flex flex-col"
|
||||
data-testid="feature-suggestions-dialog"
|
||||
>
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{currentConfig ? (
|
||||
<>
|
||||
<currentConfig.icon className={`w-5 h-5 ${currentConfig.color}`} />
|
||||
{currentConfig.label}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lightbulb className="w-5 h-5 text-yellow-500" />
|
||||
AI Suggestions
|
||||
</>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{currentConfig
|
||||
? currentConfig.description
|
||||
: 'Analyze your project to discover improvements. Choose a suggestion type below.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!hasStarted ? (
|
||||
// Initial state - show suggestion type buttons
|
||||
<div className="flex-1 flex flex-col items-center justify-center py-8">
|
||||
<p className="text-muted-foreground text-center max-w-lg mb-8">
|
||||
Our AI will analyze your project and generate actionable suggestions. Choose what type
|
||||
of analysis you want to perform:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4 w-full max-w-2xl">
|
||||
{(
|
||||
Object.entries(suggestionTypeConfig) as [
|
||||
SuggestionType,
|
||||
(typeof suggestionTypeConfig)[SuggestionType],
|
||||
][]
|
||||
).map(([type, config]) => {
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<Button
|
||||
key={type}
|
||||
variant="outline"
|
||||
className="h-auto py-6 px-6 flex flex-col items-center gap-3 hover:border-primary/50 transition-colors"
|
||||
onClick={() => handleGenerate(type)}
|
||||
data-testid={`generate-${type}-btn`}
|
||||
>
|
||||
<Icon className={`w-8 h-8 ${config.color}`} />
|
||||
<div className="text-center">
|
||||
<div className="font-semibold">
|
||||
{config.label.replace(' Suggestions', '')}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">{config.description}</div>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : isGenerating ? (
|
||||
// Generating state - show progress
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Analyzing project...
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('parsed')}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium transition-all ${
|
||||
viewMode === 'parsed'
|
||||
? 'bg-primary/20 text-primary shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
}`}
|
||||
data-testid="view-mode-parsed"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
Logs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('raw')}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium transition-all ${
|
||||
viewMode === 'raw'
|
||||
? 'bg-primary/20 text-primary shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
}`}
|
||||
data-testid="view-mode-raw"
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
Raw
|
||||
</button>
|
||||
</div>
|
||||
<Button variant="destructive" size="sm" onClick={handleStop}>
|
||||
<StopCircle className="w-4 h-4 mr-2" />
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[200px] max-h-[400px]"
|
||||
>
|
||||
{progress.length === 0 ? (
|
||||
<div className="flex items-center justify-center min-h-[168px] text-muted-foreground">
|
||||
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
||||
Waiting for AI response...
|
||||
</div>
|
||||
) : viewMode === 'parsed' ? (
|
||||
<LogViewer output={progress.join('')} />
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap break-words text-zinc-300">
|
||||
{progress.join('')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : hasSuggestions ? (
|
||||
// Results state - show suggestions list
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{suggestions.length} suggestions generated
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={toggleSelectAll}>
|
||||
{selectedIds.size === suggestions.length ? 'Deselect All' : 'Select All'}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="text-sm font-medium">{selectedIds.size} selected</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto space-y-2 min-h-[200px] max-h-[400px] pr-2"
|
||||
>
|
||||
{suggestions.map((suggestion) => {
|
||||
const isSelected = selectedIds.has(suggestion.id);
|
||||
const isExpanded = expandedIds.has(suggestion.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
className={`border rounded-lg p-3 transition-colors ${
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
data-testid={`suggestion-${suggestion.id}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id={suggestion.id}
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => toggleSelection(suggestion.id)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<button
|
||||
onClick={() => toggleExpanded(suggestion.id)}
|
||||
className="flex items-center gap-1 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/20 text-primary font-medium">
|
||||
#{suggestion.priority}
|
||||
</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-secondary text-secondary-foreground">
|
||||
{suggestion.category}
|
||||
</span>
|
||||
</div>
|
||||
<Label
|
||||
htmlFor={suggestion.id}
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
{suggestion.description}
|
||||
</Label>
|
||||
|
||||
{isExpanded && suggestion.reasoning && (
|
||||
<div className="mt-3 text-sm">
|
||||
<p className="text-muted-foreground italic">{suggestion.reasoning}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// No results state
|
||||
<div className="flex-1 flex flex-col items-center justify-center py-8 text-center">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
No suggestions were generated. Try running the analysis again.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBackToSelection}>
|
||||
Back to Selection
|
||||
</Button>
|
||||
{currentSuggestionType && (
|
||||
<Button onClick={() => handleGenerate(currentSuggestionType)}>
|
||||
<Lightbulb className="w-4 h-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="flex-shrink-0">
|
||||
{hasSuggestions && (
|
||||
<div className="flex gap-2 w-full justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBackToSelection}>
|
||||
Back
|
||||
</Button>
|
||||
{currentSuggestionType && (
|
||||
<Button variant="outline" onClick={() => handleGenerate(currentSuggestionType)}>
|
||||
{currentConfig && <currentConfig.icon className="w-4 h-4 mr-2" />}
|
||||
Regenerate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={handleImport}
|
||||
disabled={selectedIds.size === 0 || isImporting}
|
||||
hotkey={{ key: 'Enter', cmdCtrl: true }}
|
||||
hotkeyActive={open && hasSuggestions}
|
||||
>
|
||||
{isImporting ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Import {selectedIds.size} Feature
|
||||
{selectedIds.size !== 1 ? 's' : ''}
|
||||
</HotkeyButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!hasSuggestions && !isGenerating && hasStarted && (
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,5 @@ export { CompletedFeaturesModal } from './completed-features-modal';
|
||||
export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog';
|
||||
export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog';
|
||||
export { EditFeatureDialog } from './edit-feature-dialog';
|
||||
export { FeatureSuggestionsDialog } from './feature-suggestions-dialog';
|
||||
export { FollowUpDialog } from './follow-up-dialog';
|
||||
export { PlanApprovalDialog } from './plan-approval-dialog';
|
||||
|
||||
@@ -7,4 +7,3 @@ export { useBoardEffects } from './use-board-effects';
|
||||
export { useBoardBackground } from './use-board-background';
|
||||
export { useBoardPersistence } from './use-board-persistence';
|
||||
export { useFollowUpState } from './use-follow-up-state';
|
||||
export { useSuggestionsState } from './use-suggestions-state';
|
||||
|
||||
@@ -6,9 +6,6 @@ interface UseBoardEffectsProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
specCreatingForProject: string | null;
|
||||
setSpecCreatingForProject: (path: string | null) => void;
|
||||
setSuggestionsCount: (count: number) => void;
|
||||
setFeatureSuggestions: (suggestions: any[]) => void;
|
||||
setIsGeneratingSuggestions: (generating: boolean) => void;
|
||||
checkContextExists: (featureId: string) => Promise<boolean>;
|
||||
features: any[];
|
||||
isLoading: boolean;
|
||||
@@ -20,9 +17,6 @@ export function useBoardEffects({
|
||||
currentProject,
|
||||
specCreatingForProject,
|
||||
setSpecCreatingForProject,
|
||||
setSuggestionsCount,
|
||||
setFeatureSuggestions,
|
||||
setIsGeneratingSuggestions,
|
||||
checkContextExists,
|
||||
features,
|
||||
isLoading,
|
||||
@@ -44,26 +38,6 @@ export function useBoardEffects({
|
||||
};
|
||||
}, [currentProject]);
|
||||
|
||||
// Listen for suggestions events to update count (persists even when dialog is closed)
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.suggestions) return;
|
||||
|
||||
const unsubscribe = api.suggestions.onEvent((event) => {
|
||||
if (event.type === 'suggestions_complete' && event.suggestions) {
|
||||
setSuggestionsCount(event.suggestions.length);
|
||||
setFeatureSuggestions(event.suggestions);
|
||||
setIsGeneratingSuggestions(false);
|
||||
} else if (event.type === 'suggestions_error') {
|
||||
setIsGeneratingSuggestions(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [setSuggestionsCount, setFeatureSuggestions, setIsGeneratingSuggestions]);
|
||||
|
||||
// Subscribe to spec regeneration events to clear creating state on completion
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { FeatureSuggestion } from '@/lib/electron';
|
||||
|
||||
export function useSuggestionsState() {
|
||||
const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false);
|
||||
const [suggestionsCount, setSuggestionsCount] = useState(0);
|
||||
const [featureSuggestions, setFeatureSuggestions] = useState<FeatureSuggestion[]>([]);
|
||||
const [isGeneratingSuggestions, setIsGeneratingSuggestions] = useState(false);
|
||||
|
||||
const updateSuggestions = useCallback((suggestions: FeatureSuggestion[]) => {
|
||||
setFeatureSuggestions(suggestions);
|
||||
setSuggestionsCount(suggestions.length);
|
||||
}, []);
|
||||
|
||||
const closeSuggestionsDialog = useCallback(() => {
|
||||
setShowSuggestionsDialog(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
showSuggestionsDialog,
|
||||
suggestionsCount,
|
||||
featureSuggestions,
|
||||
isGeneratingSuggestions,
|
||||
// Setters
|
||||
setShowSuggestionsDialog,
|
||||
setSuggestionsCount,
|
||||
setFeatureSuggestions,
|
||||
setIsGeneratingSuggestions,
|
||||
// Helpers
|
||||
updateSuggestions,
|
||||
closeSuggestionsDialog,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||
import { KanbanColumn, KanbanCard } from './components';
|
||||
import { Feature } from '@/store/app-store';
|
||||
import { FastForward, Lightbulb, Archive, Plus, Settings2 } from 'lucide-react';
|
||||
import { FastForward, Archive, Plus, Settings2 } from 'lucide-react';
|
||||
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
||||
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
||||
import { getColumnsWithPipeline, type Column, type ColumnId } from './constants';
|
||||
@@ -47,8 +47,6 @@ interface KanbanBoardProps {
|
||||
runningAutoTasks: string[];
|
||||
shortcuts: ReturnType<typeof useKeyboardShortcutsConfig>;
|
||||
onStartNextFeatures: () => void;
|
||||
onShowSuggestions: () => void;
|
||||
suggestionsCount: number;
|
||||
onArchiveAllVerified: () => void;
|
||||
pipelineConfig: PipelineConfig | null;
|
||||
onOpenPipelineSettings?: () => void;
|
||||
@@ -82,8 +80,6 @@ export function KanbanBoard({
|
||||
runningAutoTasks,
|
||||
shortcuts,
|
||||
onStartNextFeatures,
|
||||
onShowSuggestions,
|
||||
suggestionsCount,
|
||||
onArchiveAllVerified,
|
||||
pipelineConfig,
|
||||
onOpenPipelineSettings,
|
||||
@@ -130,40 +126,20 @@ export function KanbanBoard({
|
||||
Complete All
|
||||
</Button>
|
||||
) : column.id === 'backlog' ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
columnFeatures.length > 0 && (
|
||||
<HotkeyButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10 relative"
|
||||
onClick={onShowSuggestions}
|
||||
title="Feature Suggestions"
|
||||
data-testid="feature-suggestions-button"
|
||||
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
|
||||
onClick={onStartNextFeatures}
|
||||
hotkey={shortcuts.startNext}
|
||||
hotkeyActive={false}
|
||||
data-testid="start-next-button"
|
||||
>
|
||||
<Lightbulb className="w-3.5 h-3.5" />
|
||||
{suggestionsCount > 0 && (
|
||||
<span
|
||||
className="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-mono rounded-full bg-yellow-500 text-black flex items-center justify-center"
|
||||
data-testid="suggestions-count"
|
||||
>
|
||||
{suggestionsCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{columnFeatures.length > 0 && (
|
||||
<HotkeyButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
|
||||
onClick={onStartNextFeatures}
|
||||
hotkey={shortcuts.startNext}
|
||||
hotkeyActive={false}
|
||||
data-testid="start-next-button"
|
||||
>
|
||||
<FastForward className="w-3 h-3 mr-1" />
|
||||
Make
|
||||
</HotkeyButton>
|
||||
)}
|
||||
</div>
|
||||
<FastForward className="w-3 h-3 mr-1" />
|
||||
Make
|
||||
</HotkeyButton>
|
||||
)
|
||||
) : column.id === 'in_progress' ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* IdeationDashboard - Main dashboard showing all generated suggestions
|
||||
* First page users see - shows all ideas ready for accept/reject
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Loader2, AlertCircle, Plus, X, Sparkles, Lightbulb } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useIdeationStore, type GenerationJob } from '@/store/ideation-store';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AnalysisSuggestion } from '@automaker/types';
|
||||
|
||||
interface IdeationDashboardProps {
|
||||
onGenerateIdeas: () => void;
|
||||
}
|
||||
|
||||
function SuggestionCard({
|
||||
suggestion,
|
||||
job,
|
||||
onAccept,
|
||||
onRemove,
|
||||
isAdding,
|
||||
}: {
|
||||
suggestion: AnalysisSuggestion;
|
||||
job: GenerationJob;
|
||||
onAccept: () => void;
|
||||
onRemove: () => void;
|
||||
isAdding: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card className="transition-all hover:border-primary/50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium">{suggestion.title}</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{suggestion.priority}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{job.prompt.title}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{suggestion.description}</p>
|
||||
{suggestion.rationale && (
|
||||
<p className="text-xs text-muted-foreground mt-2 italic">{suggestion.rationale}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onRemove}
|
||||
disabled={isAdding}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" onClick={onAccept} disabled={isAdding} className="gap-1">
|
||||
{isAdding ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4" />
|
||||
Accept
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function GeneratingCard({ job }: { job: GenerationJob }) {
|
||||
const { removeJob } = useIdeationStore();
|
||||
const isError = job.status === 'error';
|
||||
|
||||
return (
|
||||
<Card className={cn('transition-all', isError ? 'border-red-500/50' : 'border-blue-500/50')}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{isError ? (
|
||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||
) : (
|
||||
<Loader2 className="w-5 h-5 text-blue-500 animate-spin" />
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium">{job.prompt.title}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isError ? job.error || 'Failed to generate' : 'Generating ideas...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeJob(job.id)}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TagFilter({
|
||||
tags,
|
||||
tagCounts,
|
||||
selectedTags,
|
||||
onToggleTag,
|
||||
}: {
|
||||
tags: string[];
|
||||
tagCounts: Record<string, number>;
|
||||
selectedTags: Set<string>;
|
||||
onToggleTag: (tag: string) => void;
|
||||
}) {
|
||||
if (tags.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => {
|
||||
const isSelected = selectedTags.has(tag);
|
||||
const count = tagCounts[tag] || 0;
|
||||
return (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => onToggleTag(tag)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 text-sm rounded-full border transition-all flex items-center gap-1.5',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-secondary/50 text-muted-foreground border-border hover:border-primary/50 hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{tag}
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs',
|
||||
isSelected ? 'text-primary-foreground/70' : 'text-muted-foreground/70'
|
||||
)}
|
||||
>
|
||||
({count})
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{selectedTags.size > 0 && (
|
||||
<button
|
||||
onClick={() => selectedTags.forEach((tag) => onToggleTag(tag))}
|
||||
className="px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function IdeationDashboard({ onGenerateIdeas }: IdeationDashboardProps) {
|
||||
const currentProject = useAppStore((s) => s.currentProject);
|
||||
const { generationJobs, removeSuggestionFromJob } = useIdeationStore();
|
||||
const [addingId, setAddingId] = useState<string | null>(null);
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
|
||||
// Separate generating/error jobs from ready jobs with suggestions
|
||||
const activeJobs = generationJobs.filter(
|
||||
(j) => j.status === 'generating' || j.status === 'error'
|
||||
);
|
||||
const readyJobs = generationJobs.filter((j) => j.status === 'ready' && j.suggestions.length > 0);
|
||||
|
||||
// Flatten all suggestions with their parent job
|
||||
const allSuggestions = useMemo(
|
||||
() => readyJobs.flatMap((job) => job.suggestions.map((suggestion) => ({ suggestion, job }))),
|
||||
[readyJobs]
|
||||
);
|
||||
|
||||
// Extract unique tags and counts from all suggestions
|
||||
const { availableTags, tagCounts } = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
allSuggestions.forEach(({ job }) => {
|
||||
const tag = job.prompt.title;
|
||||
counts[tag] = (counts[tag] || 0) + 1;
|
||||
});
|
||||
return {
|
||||
availableTags: Object.keys(counts).sort(),
|
||||
tagCounts: counts,
|
||||
};
|
||||
}, [allSuggestions]);
|
||||
|
||||
// Filter suggestions based on selected tags
|
||||
const filteredSuggestions = useMemo(() => {
|
||||
if (selectedTags.size === 0) return allSuggestions;
|
||||
return allSuggestions.filter(({ job }) => selectedTags.has(job.prompt.title));
|
||||
}, [allSuggestions, selectedTags]);
|
||||
|
||||
const generatingCount = generationJobs.filter((j) => j.status === 'generating').length;
|
||||
|
||||
const handleToggleTag = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(tag)) {
|
||||
next.delete(tag);
|
||||
} else {
|
||||
next.add(tag);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAccept = async (suggestion: AnalysisSuggestion, jobId: string) => {
|
||||
if (!currentProject?.path) {
|
||||
toast.error('No project selected');
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingId(suggestion.id);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.ideation?.addSuggestionToBoard(currentProject.path, suggestion);
|
||||
|
||||
if (result?.success) {
|
||||
toast.success(`Added "${suggestion.title}" to board`);
|
||||
removeSuggestionFromJob(jobId, suggestion.id);
|
||||
} else {
|
||||
toast.error(result?.error || 'Failed to add to board');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add to board:', error);
|
||||
toast.error((error as Error).message);
|
||||
} finally {
|
||||
setAddingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (suggestionId: string, jobId: string) => {
|
||||
removeSuggestionFromJob(jobId, suggestionId);
|
||||
toast.info('Idea removed');
|
||||
};
|
||||
|
||||
const isEmpty = allSuggestions.length === 0 && activeJobs.length === 0;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col p-6 overflow-auto">
|
||||
<div className="max-w-3xl w-full mx-auto space-y-4">
|
||||
{/* Status text */}
|
||||
{(generatingCount > 0 || allSuggestions.length > 0) && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{generatingCount > 0
|
||||
? `Generating ${generatingCount} idea${generatingCount > 1 ? 's' : ''}...`
|
||||
: selectedTags.size > 0
|
||||
? `Showing ${filteredSuggestions.length} of ${allSuggestions.length} ideas`
|
||||
: `${allSuggestions.length} idea${allSuggestions.length > 1 ? 's' : ''} ready for review`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Tag Filters */}
|
||||
{availableTags.length > 0 && (
|
||||
<TagFilter
|
||||
tags={availableTags}
|
||||
tagCounts={tagCounts}
|
||||
selectedTags={selectedTags}
|
||||
onToggleTag={handleToggleTag}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Generating/Error Jobs */}
|
||||
{activeJobs.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{activeJobs.map((job) => (
|
||||
<GeneratingCard key={job.id} job={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Suggestions List */}
|
||||
{filteredSuggestions.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{filteredSuggestions.map(({ suggestion, job }) => (
|
||||
<SuggestionCard
|
||||
key={suggestion.id}
|
||||
suggestion={suggestion}
|
||||
job={job}
|
||||
onAccept={() => handleAccept(suggestion, job.id)}
|
||||
onRemove={() => handleRemove(suggestion.id, job.id)}
|
||||
isAdding={addingId === suggestion.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No results after filtering */}
|
||||
{filteredSuggestions.length === 0 && allSuggestions.length > 0 && (
|
||||
<Card>
|
||||
<CardContent className="py-8">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>No ideas match the selected filters</p>
|
||||
<button
|
||||
onClick={() => setSelectedTags(new Set())}
|
||||
className="text-primary hover:underline mt-2"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{isEmpty && (
|
||||
<Card>
|
||||
<CardContent className="py-16">
|
||||
<div className="text-center">
|
||||
<Sparkles className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">No ideas yet</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Generate ideas by selecting a category and prompt type
|
||||
</p>
|
||||
<Button onClick={onGenerateIdeas} size="lg" className="gap-2">
|
||||
<Lightbulb className="w-5 h-5" />
|
||||
Generate Ideas
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* PromptCategoryGrid - Grid of prompt categories to select from
|
||||
*/
|
||||
|
||||
import {
|
||||
ArrowLeft,
|
||||
Zap,
|
||||
Palette,
|
||||
Code,
|
||||
TrendingUp,
|
||||
Cpu,
|
||||
Shield,
|
||||
Gauge,
|
||||
Accessibility,
|
||||
BarChart3,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { PROMPT_CATEGORIES } from '../data/guided-prompts';
|
||||
import type { IdeaCategory } from '@automaker/types';
|
||||
|
||||
interface PromptCategoryGridProps {
|
||||
onSelect: (category: IdeaCategory) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const iconMap: Record<string, typeof Zap> = {
|
||||
Zap,
|
||||
Palette,
|
||||
Code,
|
||||
TrendingUp,
|
||||
Cpu,
|
||||
Shield,
|
||||
Gauge,
|
||||
Accessibility,
|
||||
BarChart3,
|
||||
};
|
||||
|
||||
export function PromptCategoryGrid({ onSelect, onBack }: PromptCategoryGridProps) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col p-6 overflow-auto">
|
||||
<div className="max-w-4xl w-full mx-auto space-y-4">
|
||||
{/* Back link */}
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{PROMPT_CATEGORIES.map((category) => {
|
||||
const Icon = iconMap[category.icon] || Zap;
|
||||
return (
|
||||
<Card
|
||||
key={category.id}
|
||||
className="cursor-pointer transition-all hover:border-primary hover:shadow-md"
|
||||
onClick={() => onSelect(category.id)}
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col items-center text-center gap-3">
|
||||
<div className="p-4 rounded-full bg-primary/10">
|
||||
<Icon className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{category.name}</h3>
|
||||
<p className="text-muted-foreground text-sm mt-1">{category.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* PromptList - List of prompts for a specific category
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ArrowLeft, Lightbulb, Loader2, CheckCircle2 } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { getPromptsByCategory } from '../data/guided-prompts';
|
||||
import { useIdeationStore } from '@/store/ideation-store';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import type { IdeaCategory, IdeationPrompt } from '@automaker/types';
|
||||
|
||||
interface PromptListProps {
|
||||
category: IdeaCategory;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function PromptList({ category, onBack }: PromptListProps) {
|
||||
const currentProject = useAppStore((s) => s.currentProject);
|
||||
const { setMode, addGenerationJob, updateJobStatus, generationJobs } = useIdeationStore();
|
||||
const [loadingPromptId, setLoadingPromptId] = useState<string | null>(null);
|
||||
const [startedPrompts, setStartedPrompts] = useState<Set<string>>(new Set());
|
||||
const navigate = useNavigate();
|
||||
|
||||
const prompts = getPromptsByCategory(category);
|
||||
|
||||
// Check which prompts are already generating
|
||||
const generatingPromptIds = new Set(
|
||||
generationJobs.filter((j) => j.status === 'generating').map((j) => j.prompt.id)
|
||||
);
|
||||
|
||||
const handleSelectPrompt = async (prompt: IdeationPrompt) => {
|
||||
if (!currentProject?.path) {
|
||||
toast.error('No project selected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadingPromptId || generatingPromptIds.has(prompt.id)) return;
|
||||
|
||||
setLoadingPromptId(prompt.id);
|
||||
|
||||
// Add a job and navigate to dashboard
|
||||
const jobId = addGenerationJob(prompt);
|
||||
setStartedPrompts((prev) => new Set(prev).add(prompt.id));
|
||||
|
||||
// Show toast and navigate to dashboard
|
||||
toast.info(`Generating ideas for "${prompt.title}"...`);
|
||||
setMode('dashboard');
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.ideation?.generateSuggestions(
|
||||
currentProject.path,
|
||||
prompt.id,
|
||||
category
|
||||
);
|
||||
|
||||
if (result?.success && result.suggestions) {
|
||||
updateJobStatus(jobId, 'ready', result.suggestions);
|
||||
toast.success(`Generated ${result.suggestions.length} ideas for "${prompt.title}"`, {
|
||||
duration: 10000,
|
||||
action: {
|
||||
label: 'View Ideas',
|
||||
onClick: () => {
|
||||
setMode('dashboard');
|
||||
navigate({ to: '/ideation' });
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateJobStatus(
|
||||
jobId,
|
||||
'error',
|
||||
undefined,
|
||||
result?.error || 'Failed to generate suggestions'
|
||||
);
|
||||
toast.error(result?.error || 'Failed to generate suggestions');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate suggestions:', error);
|
||||
updateJobStatus(jobId, 'error', undefined, (error as Error).message);
|
||||
toast.error((error as Error).message);
|
||||
} finally {
|
||||
setLoadingPromptId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col p-6 overflow-auto">
|
||||
<div className="max-w-3xl w-full mx-auto space-y-4">
|
||||
{/* Back link */}
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
|
||||
<div className="space-y-3">
|
||||
{prompts.map((prompt) => {
|
||||
const isLoading = loadingPromptId === prompt.id;
|
||||
const isGenerating = generatingPromptIds.has(prompt.id);
|
||||
const isStarted = startedPrompts.has(prompt.id);
|
||||
const isDisabled = loadingPromptId !== null || isGenerating;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={prompt.id}
|
||||
className={`transition-all ${
|
||||
isDisabled
|
||||
? 'opacity-60 cursor-not-allowed'
|
||||
: 'cursor-pointer hover:border-primary hover:shadow-md'
|
||||
} ${isLoading || isGenerating ? 'border-blue-500 ring-1 ring-blue-500' : ''} ${
|
||||
isStarted && !isGenerating ? 'border-green-500/50' : ''
|
||||
}`}
|
||||
onClick={() => !isDisabled && handleSelectPrompt(prompt)}
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`p-2 rounded-lg mt-0.5 ${
|
||||
isLoading || isGenerating
|
||||
? 'bg-blue-500/10'
|
||||
: isStarted
|
||||
? 'bg-green-500/10'
|
||||
: 'bg-primary/10'
|
||||
}`}
|
||||
>
|
||||
{isLoading || isGenerating ? (
|
||||
<Loader2 className="w-4 h-4 text-blue-500 animate-spin" />
|
||||
) : isStarted ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Lightbulb className="w-4 h-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold">{prompt.title}</h3>
|
||||
<p className="text-muted-foreground text-sm mt-1">{prompt.description}</p>
|
||||
{(isLoading || isGenerating) && (
|
||||
<p className="text-blue-500 text-sm mt-2">Generating in dashboard...</p>
|
||||
)}
|
||||
{isStarted && !isGenerating && (
|
||||
<p className="text-green-500 text-sm mt-2">
|
||||
Already generated - check dashboard
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* Guided prompts for ideation sessions
|
||||
* Static data that provides pre-made prompts for different categories
|
||||
*/
|
||||
|
||||
import type { IdeaCategory, IdeationPrompt, PromptCategory } from '@automaker/types';
|
||||
|
||||
export const PROMPT_CATEGORIES: PromptCategory[] = [
|
||||
{
|
||||
id: 'feature',
|
||||
name: 'Features',
|
||||
icon: 'Zap',
|
||||
description: 'New capabilities and functionality',
|
||||
},
|
||||
{
|
||||
id: 'ux-ui',
|
||||
name: 'UX/UI',
|
||||
icon: 'Palette',
|
||||
description: 'Design and user experience improvements',
|
||||
},
|
||||
{
|
||||
id: 'dx',
|
||||
name: 'Developer Experience',
|
||||
icon: 'Code',
|
||||
description: 'Developer tooling and workflows',
|
||||
},
|
||||
{
|
||||
id: 'growth',
|
||||
name: 'Growth',
|
||||
icon: 'TrendingUp',
|
||||
description: 'User engagement and retention',
|
||||
},
|
||||
{
|
||||
id: 'technical',
|
||||
name: 'Technical',
|
||||
icon: 'Cpu',
|
||||
description: 'Architecture and infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
name: 'Security',
|
||||
icon: 'Shield',
|
||||
description: 'Security and privacy improvements',
|
||||
},
|
||||
{
|
||||
id: 'performance',
|
||||
name: 'Performance',
|
||||
icon: 'Gauge',
|
||||
description: 'Speed and optimization',
|
||||
},
|
||||
{
|
||||
id: 'accessibility',
|
||||
name: 'Accessibility',
|
||||
icon: 'Accessibility',
|
||||
description: 'Inclusive design for all users',
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
name: 'Analytics',
|
||||
icon: 'BarChart3',
|
||||
description: 'Data insights and tracking',
|
||||
},
|
||||
];
|
||||
|
||||
export const GUIDED_PROMPTS: IdeationPrompt[] = [
|
||||
// Feature prompts
|
||||
{
|
||||
id: 'feature-missing',
|
||||
category: 'feature',
|
||||
title: 'Missing Features',
|
||||
description: 'Discover features users might expect',
|
||||
prompt:
|
||||
"Analyze this codebase and identify features that users of similar applications typically expect but are missing here. Consider the app's domain, target users, and common patterns in similar products.",
|
||||
},
|
||||
{
|
||||
id: 'feature-automation',
|
||||
category: 'feature',
|
||||
title: 'Automation Opportunities',
|
||||
description: 'Find manual processes that could be automated',
|
||||
prompt:
|
||||
'Review this codebase and identify manual processes or repetitive tasks that could be automated. Look for patterns where users might be doing things repeatedly that software could handle.',
|
||||
},
|
||||
{
|
||||
id: 'feature-integrations',
|
||||
category: 'feature',
|
||||
title: 'Integration Ideas',
|
||||
description: 'Identify valuable third-party integrations',
|
||||
prompt:
|
||||
"Based on this codebase, what third-party services or APIs would provide value if integrated? Consider the app's domain and what complementary services users might need.",
|
||||
},
|
||||
{
|
||||
id: 'feature-workflow',
|
||||
category: 'feature',
|
||||
title: 'Workflow Improvements',
|
||||
description: 'Streamline user workflows',
|
||||
prompt:
|
||||
'Analyze the user workflows in this application. What steps could be combined, eliminated, or automated? Where are users likely spending too much time on repetitive tasks?',
|
||||
},
|
||||
|
||||
// UX/UI prompts
|
||||
{
|
||||
id: 'ux-friction',
|
||||
category: 'ux-ui',
|
||||
title: 'Friction Points',
|
||||
description: 'Identify where users might get stuck',
|
||||
prompt:
|
||||
'Analyze the user flows in this codebase and identify potential friction points. Where might users get confused, stuck, or frustrated? Look at form submissions, navigation, error states, and complex interactions.',
|
||||
},
|
||||
{
|
||||
id: 'ux-empty-states',
|
||||
category: 'ux-ui',
|
||||
title: 'Empty States',
|
||||
description: 'Improve empty state experiences',
|
||||
prompt:
|
||||
"Review the components in this codebase and identify empty states that could be improved. How can we guide users when there's no content? Consider onboarding, helpful prompts, and sample data.",
|
||||
},
|
||||
{
|
||||
id: 'ux-accessibility',
|
||||
category: 'ux-ui',
|
||||
title: 'Accessibility Improvements',
|
||||
description: 'Enhance accessibility and inclusivity',
|
||||
prompt:
|
||||
'Analyze this codebase for accessibility improvements. Consider keyboard navigation, screen reader support, color contrast, focus states, and ARIA labels. What specific improvements would make this more accessible?',
|
||||
},
|
||||
{
|
||||
id: 'ux-mobile',
|
||||
category: 'ux-ui',
|
||||
title: 'Mobile Experience',
|
||||
description: 'Optimize for mobile users',
|
||||
prompt:
|
||||
'Review this codebase from a mobile-first perspective. What improvements would enhance the mobile user experience? Consider touch targets, responsive layouts, and mobile-specific interactions.',
|
||||
},
|
||||
{
|
||||
id: 'ux-feedback',
|
||||
category: 'ux-ui',
|
||||
title: 'User Feedback',
|
||||
description: 'Improve feedback and status indicators',
|
||||
prompt:
|
||||
'Analyze how this application communicates with users. Where are loading states, success messages, or error handling missing or unclear? What feedback would help users understand what is happening?',
|
||||
},
|
||||
|
||||
// DX prompts
|
||||
{
|
||||
id: 'dx-documentation',
|
||||
category: 'dx',
|
||||
title: 'Documentation Gaps',
|
||||
description: 'Identify missing documentation',
|
||||
prompt:
|
||||
'Review this codebase and identify areas lacking documentation. What would help new developers understand the architecture, APIs, and conventions? Consider inline comments, READMEs, and API docs.',
|
||||
},
|
||||
{
|
||||
id: 'dx-testing',
|
||||
category: 'dx',
|
||||
title: 'Testing Improvements',
|
||||
description: 'Enhance test coverage and quality',
|
||||
prompt:
|
||||
'Analyze the testing patterns in this codebase. What areas need better test coverage? What types of tests are missing? Consider unit tests, integration tests, and end-to-end tests.',
|
||||
},
|
||||
{
|
||||
id: 'dx-tooling',
|
||||
category: 'dx',
|
||||
title: 'Developer Tooling',
|
||||
description: 'Improve development workflows',
|
||||
prompt:
|
||||
'Review the development setup and tooling in this codebase. What improvements would speed up development? Consider build times, hot reload, debugging tools, and developer scripts.',
|
||||
},
|
||||
{
|
||||
id: 'dx-error-handling',
|
||||
category: 'dx',
|
||||
title: 'Error Handling',
|
||||
description: 'Improve error messages and debugging',
|
||||
prompt:
|
||||
'Analyze error handling in this codebase. Where are error messages unclear or missing? What would help developers debug issues faster? Consider logging, error boundaries, and stack traces.',
|
||||
},
|
||||
|
||||
// Growth prompts
|
||||
{
|
||||
id: 'growth-onboarding',
|
||||
category: 'growth',
|
||||
title: 'Onboarding Flow',
|
||||
description: 'Improve new user experience',
|
||||
prompt:
|
||||
"Analyze this application's onboarding experience. How can we help new users understand the value and get started quickly? Consider tutorials, progressive disclosure, and quick wins.",
|
||||
},
|
||||
{
|
||||
id: 'growth-engagement',
|
||||
category: 'growth',
|
||||
title: 'User Engagement',
|
||||
description: 'Increase user retention and activity',
|
||||
prompt:
|
||||
'Review this application and suggest features that would increase user engagement and retention. What would bring users back daily? Consider notifications, streaks, social features, and personalization.',
|
||||
},
|
||||
{
|
||||
id: 'growth-sharing',
|
||||
category: 'growth',
|
||||
title: 'Shareability',
|
||||
description: 'Make the app more shareable',
|
||||
prompt:
|
||||
'How can this application be made more shareable? What features would encourage users to invite others or share their work? Consider collaboration, public profiles, and export features.',
|
||||
},
|
||||
{
|
||||
id: 'growth-monetization',
|
||||
category: 'growth',
|
||||
title: 'Monetization Ideas',
|
||||
description: 'Identify potential revenue streams',
|
||||
prompt:
|
||||
'Based on this codebase, what features or tiers could support monetization? Consider premium features, usage limits, team features, and integrations that users would pay for.',
|
||||
},
|
||||
|
||||
// Technical prompts
|
||||
{
|
||||
id: 'tech-performance',
|
||||
category: 'technical',
|
||||
title: 'Performance Optimization',
|
||||
description: 'Identify performance bottlenecks',
|
||||
prompt:
|
||||
'Analyze this codebase for performance optimization opportunities. Where are the likely bottlenecks? Consider database queries, API calls, bundle size, rendering, and caching strategies.',
|
||||
},
|
||||
{
|
||||
id: 'tech-architecture',
|
||||
category: 'technical',
|
||||
title: 'Architecture Review',
|
||||
description: 'Evaluate and improve architecture',
|
||||
prompt:
|
||||
'Review the architecture of this codebase. What improvements would make it more maintainable, scalable, or testable? Consider separation of concerns, dependency management, and patterns.',
|
||||
},
|
||||
{
|
||||
id: 'tech-debt',
|
||||
category: 'technical',
|
||||
title: 'Technical Debt',
|
||||
description: 'Identify areas needing refactoring',
|
||||
prompt:
|
||||
'Identify technical debt in this codebase. What areas are becoming hard to maintain or understand? What refactoring would have the highest impact? Consider duplicated code, complexity, and outdated patterns.',
|
||||
},
|
||||
{
|
||||
id: 'tech-security',
|
||||
category: 'technical',
|
||||
title: 'Security Review',
|
||||
description: 'Identify security improvements',
|
||||
prompt:
|
||||
'Review this codebase for security improvements. What best practices are missing? Consider authentication, authorization, input validation, and data protection. Note: This is for improvement suggestions, not a security audit.',
|
||||
},
|
||||
|
||||
// Security prompts
|
||||
{
|
||||
id: 'security-auth',
|
||||
category: 'security',
|
||||
title: 'Authentication Security',
|
||||
description: 'Review authentication mechanisms',
|
||||
prompt:
|
||||
'Analyze the authentication system in this codebase. What security improvements would strengthen user authentication? Consider password policies, session management, MFA, and token handling.',
|
||||
},
|
||||
{
|
||||
id: 'security-data',
|
||||
category: 'security',
|
||||
title: 'Data Protection',
|
||||
description: 'Protect sensitive user data',
|
||||
prompt:
|
||||
'Review how this application handles sensitive data. What improvements would better protect user privacy? Consider encryption, data minimization, secure storage, and data retention policies.',
|
||||
},
|
||||
{
|
||||
id: 'security-input',
|
||||
category: 'security',
|
||||
title: 'Input Validation',
|
||||
description: 'Prevent injection attacks',
|
||||
prompt:
|
||||
'Analyze input handling in this codebase. Where could input validation be strengthened? Consider SQL injection, XSS, command injection, and file upload vulnerabilities.',
|
||||
},
|
||||
{
|
||||
id: 'security-api',
|
||||
category: 'security',
|
||||
title: 'API Security',
|
||||
description: 'Secure API endpoints',
|
||||
prompt:
|
||||
'Review the API security in this codebase. What improvements would make the API more secure? Consider rate limiting, authorization, CORS, and request validation.',
|
||||
},
|
||||
|
||||
// Performance prompts
|
||||
{
|
||||
id: 'perf-frontend',
|
||||
category: 'performance',
|
||||
title: 'Frontend Performance',
|
||||
description: 'Optimize UI rendering and loading',
|
||||
prompt:
|
||||
'Analyze the frontend performance of this application. What optimizations would improve load times and responsiveness? Consider bundle splitting, lazy loading, memoization, and render optimization.',
|
||||
},
|
||||
{
|
||||
id: 'perf-backend',
|
||||
category: 'performance',
|
||||
title: 'Backend Performance',
|
||||
description: 'Optimize server-side operations',
|
||||
prompt:
|
||||
'Review backend performance in this codebase. What optimizations would improve response times? Consider database queries, caching strategies, async operations, and resource pooling.',
|
||||
},
|
||||
{
|
||||
id: 'perf-database',
|
||||
category: 'performance',
|
||||
title: 'Database Optimization',
|
||||
description: 'Improve query performance',
|
||||
prompt:
|
||||
'Analyze database interactions in this codebase. What optimizations would improve data access performance? Consider indexing, query optimization, denormalization, and connection pooling.',
|
||||
},
|
||||
{
|
||||
id: 'perf-caching',
|
||||
category: 'performance',
|
||||
title: 'Caching Strategies',
|
||||
description: 'Implement effective caching',
|
||||
prompt:
|
||||
'Review caching opportunities in this application. Where would caching provide the most benefit? Consider API responses, computed values, static assets, and session data.',
|
||||
},
|
||||
|
||||
// Accessibility prompts
|
||||
{
|
||||
id: 'a11y-keyboard',
|
||||
category: 'accessibility',
|
||||
title: 'Keyboard Navigation',
|
||||
description: 'Enable full keyboard access',
|
||||
prompt:
|
||||
'Analyze keyboard accessibility in this codebase. What improvements would enable users to navigate entirely with keyboard? Consider focus management, tab order, and keyboard shortcuts.',
|
||||
},
|
||||
{
|
||||
id: 'a11y-screen-reader',
|
||||
category: 'accessibility',
|
||||
title: 'Screen Reader Support',
|
||||
description: 'Improve screen reader experience',
|
||||
prompt:
|
||||
'Review screen reader compatibility in this application. What improvements would help users with visual impairments? Consider ARIA labels, semantic HTML, live regions, and alt text.',
|
||||
},
|
||||
{
|
||||
id: 'a11y-visual',
|
||||
category: 'accessibility',
|
||||
title: 'Visual Accessibility',
|
||||
description: 'Improve visual design for all users',
|
||||
prompt:
|
||||
'Analyze visual accessibility in this codebase. What improvements would help users with visual impairments? Consider color contrast, text sizing, focus indicators, and reduced motion.',
|
||||
},
|
||||
{
|
||||
id: 'a11y-forms',
|
||||
category: 'accessibility',
|
||||
title: 'Accessible Forms',
|
||||
description: 'Make forms usable for everyone',
|
||||
prompt:
|
||||
'Review form accessibility in this application. What improvements would make forms more accessible? Consider labels, error messages, required field indicators, and input assistance.',
|
||||
},
|
||||
|
||||
// Analytics prompts
|
||||
{
|
||||
id: 'analytics-tracking',
|
||||
category: 'analytics',
|
||||
title: 'User Tracking',
|
||||
description: 'Track key user behaviors',
|
||||
prompt:
|
||||
'Analyze this application for analytics opportunities. What user behaviors should be tracked to understand engagement? Consider page views, feature usage, conversion funnels, and session duration.',
|
||||
},
|
||||
{
|
||||
id: 'analytics-metrics',
|
||||
category: 'analytics',
|
||||
title: 'Key Metrics',
|
||||
description: 'Define success metrics',
|
||||
prompt:
|
||||
'Based on this codebase, what key metrics should be tracked? Consider user acquisition, retention, engagement, and feature adoption. What dashboards would be most valuable?',
|
||||
},
|
||||
{
|
||||
id: 'analytics-errors',
|
||||
category: 'analytics',
|
||||
title: 'Error Monitoring',
|
||||
description: 'Track and analyze errors',
|
||||
prompt:
|
||||
'Review error handling in this codebase for monitoring opportunities. What error tracking would help identify and fix issues faster? Consider error aggregation, alerting, and stack traces.',
|
||||
},
|
||||
{
|
||||
id: 'analytics-performance',
|
||||
category: 'analytics',
|
||||
title: 'Performance Monitoring',
|
||||
description: 'Track application performance',
|
||||
prompt:
|
||||
'Analyze this application for performance monitoring opportunities. What metrics would help identify bottlenecks? Consider load times, API response times, and resource usage.',
|
||||
},
|
||||
];
|
||||
|
||||
export function getPromptsByCategory(category: IdeaCategory): IdeationPrompt[] {
|
||||
return GUIDED_PROMPTS.filter((p) => p.category === category);
|
||||
}
|
||||
|
||||
export function getPromptById(id: string): IdeationPrompt | undefined {
|
||||
return GUIDED_PROMPTS.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
export function getCategoryById(id: IdeaCategory): PromptCategory | undefined {
|
||||
return PROMPT_CATEGORIES.find((c) => c.id === id);
|
||||
}
|
||||
208
apps/ui/src/components/views/ideation-view/index.tsx
Normal file
208
apps/ui/src/components/views/ideation-view/index.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* IdeationView - Main view for brainstorming and idea management
|
||||
* Dashboard-first design with Generate Ideas flow
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useIdeationStore } from '@/store/ideation-store';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { PromptCategoryGrid } from './components/prompt-category-grid';
|
||||
import { PromptList } from './components/prompt-list';
|
||||
import { IdeationDashboard } from './components/ideation-dashboard';
|
||||
import { getCategoryById } from './data/guided-prompts';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowLeft, ChevronRight, Lightbulb } from 'lucide-react';
|
||||
import type { IdeaCategory } from '@automaker/types';
|
||||
import type { IdeationMode } from '@/store/ideation-store';
|
||||
|
||||
// Get subtitle text based on current mode
|
||||
function getSubtitle(currentMode: IdeationMode, selectedCategory: IdeaCategory | null): string {
|
||||
if (currentMode === 'dashboard') {
|
||||
return 'Review and accept generated ideas';
|
||||
}
|
||||
if (currentMode === 'prompts') {
|
||||
if (selectedCategory) {
|
||||
const categoryInfo = getCategoryById(selectedCategory);
|
||||
return `Select a prompt from ${categoryInfo?.name || 'category'}`;
|
||||
}
|
||||
return 'Select a category to generate ideas';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Breadcrumb component - compact inline breadcrumbs
|
||||
function IdeationBreadcrumbs({
|
||||
currentMode,
|
||||
selectedCategory,
|
||||
onNavigate,
|
||||
}: {
|
||||
currentMode: IdeationMode;
|
||||
selectedCategory: IdeaCategory | null;
|
||||
onNavigate: (mode: IdeationMode, category?: IdeaCategory | null) => void;
|
||||
}) {
|
||||
const categoryInfo = selectedCategory ? getCategoryById(selectedCategory) : null;
|
||||
|
||||
// On dashboard, no breadcrumbs needed (it's the root)
|
||||
if (currentMode === 'dashboard') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<button
|
||||
onClick={() => onNavigate('dashboard')}
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
{selectedCategory && categoryInfo ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onNavigate('prompts', null)}
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Generate Ideas
|
||||
</button>
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
<span className="text-foreground">{categoryInfo.name}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-foreground">Generate Ideas</span>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// Header shown on all pages - matches other view headers
|
||||
function IdeationHeader({
|
||||
currentMode,
|
||||
selectedCategory,
|
||||
onNavigate,
|
||||
onGenerateIdeas,
|
||||
onBack,
|
||||
}: {
|
||||
currentMode: IdeationMode;
|
||||
selectedCategory: IdeaCategory | null;
|
||||
onNavigate: (mode: IdeationMode, category?: IdeaCategory | null) => void;
|
||||
onGenerateIdeas: () => void;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const subtitle = getSubtitle(currentMode, selectedCategory);
|
||||
const showBackButton = currentMode === 'prompts';
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
{showBackButton && (
|
||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Lightbulb className="w-5 h-5 text-primary" />
|
||||
<h1 className="text-xl font-bold">Ideation</h1>
|
||||
</div>
|
||||
{currentMode === 'dashboard' ? (
|
||||
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
||||
) : (
|
||||
<IdeationBreadcrumbs
|
||||
currentMode={currentMode}
|
||||
selectedCategory={selectedCategory}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button onClick={onGenerateIdeas} className="gap-2">
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
Generate Ideas
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function IdeationView() {
|
||||
const currentProject = useAppStore((s) => s.currentProject);
|
||||
const { currentMode, selectedCategory, setMode, setCategory } = useIdeationStore();
|
||||
|
||||
const handleNavigate = useCallback(
|
||||
(mode: IdeationMode, category?: IdeaCategory | null) => {
|
||||
setMode(mode);
|
||||
if (category !== undefined) {
|
||||
setCategory(category);
|
||||
} else if (mode !== 'prompts') {
|
||||
setCategory(null);
|
||||
}
|
||||
},
|
||||
[setMode, setCategory]
|
||||
);
|
||||
|
||||
const handleSelectCategory = useCallback(
|
||||
(category: IdeaCategory) => {
|
||||
setCategory(category);
|
||||
},
|
||||
[setCategory]
|
||||
);
|
||||
|
||||
const handleBackFromPrompts = useCallback(() => {
|
||||
// If viewing a category, go back to category grid
|
||||
if (selectedCategory) {
|
||||
setCategory(null);
|
||||
return;
|
||||
}
|
||||
// Otherwise, go back to dashboard
|
||||
setMode('dashboard');
|
||||
}, [selectedCategory, setCategory, setMode]);
|
||||
|
||||
const handleGenerateIdeas = useCallback(() => {
|
||||
setMode('prompts');
|
||||
setCategory(null);
|
||||
}, [setMode, setCategory]);
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center content-bg"
|
||||
data-testid="ideation-view"
|
||||
>
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>Open a project to start brainstorming ideas</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col content-bg min-h-0 overflow-hidden"
|
||||
data-testid="ideation-view"
|
||||
>
|
||||
{/* Header with breadcrumbs - always shown */}
|
||||
<IdeationHeader
|
||||
currentMode={currentMode}
|
||||
selectedCategory={selectedCategory}
|
||||
onNavigate={handleNavigate}
|
||||
onGenerateIdeas={handleGenerateIdeas}
|
||||
onBack={handleBackFromPrompts}
|
||||
/>
|
||||
|
||||
{/* Dashboard - main view */}
|
||||
{currentMode === 'dashboard' && <IdeationDashboard onGenerateIdeas={handleGenerateIdeas} />}
|
||||
|
||||
{/* Prompts - category selection */}
|
||||
{currentMode === 'prompts' && !selectedCategory && (
|
||||
<PromptCategoryGrid onSelect={handleSelectCategory} onBack={handleBackFromPrompts} />
|
||||
)}
|
||||
|
||||
{/* Prompts - prompt selection within category */}
|
||||
{currentMode === 'prompts' && selectedCategory && (
|
||||
<PromptList category={selectedCategory} onBack={handleBackFromPrompts} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useAppStore, parseShortcut } from '@/store/app-store';
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useAppStore, parseShortcut, DEFAULT_KEYBOARD_SHORTCUTS } from '@/store/app-store';
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
key: string; // Can be simple "K" or with modifiers "Shift+N", "Cmd+K"
|
||||
@@ -237,8 +237,18 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
|
||||
/**
|
||||
* Hook to get current keyboard shortcuts from store
|
||||
* This replaces the static constants and allows customization
|
||||
* Merges with defaults to ensure new shortcuts are always available
|
||||
*/
|
||||
export function useKeyboardShortcutsConfig() {
|
||||
const keyboardShortcuts = useAppStore((state) => state.keyboardShortcuts);
|
||||
return keyboardShortcuts;
|
||||
|
||||
// Merge with defaults to ensure new shortcuts are available
|
||||
// even if user's persisted state predates them
|
||||
return useMemo(
|
||||
() => ({
|
||||
...DEFAULT_KEYBOARD_SHORTCUTS,
|
||||
...keyboardShortcuts,
|
||||
}),
|
||||
[keyboardShortcuts]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@ import type {
|
||||
AgentModel,
|
||||
GitHubComment,
|
||||
IssueCommentsResult,
|
||||
Idea,
|
||||
IdeaCategory,
|
||||
IdeationSession,
|
||||
IdeationMessage,
|
||||
ProjectAnalysisResult,
|
||||
AnalysisSuggestion,
|
||||
StartSessionOptions,
|
||||
CreateIdeaInput,
|
||||
UpdateIdeaInput,
|
||||
ConvertToFeatureOptions,
|
||||
} from '@automaker/types';
|
||||
import { getJSON, setJSON, removeItem } from './storage';
|
||||
|
||||
@@ -30,6 +40,94 @@ export type {
|
||||
IssueCommentsResult,
|
||||
};
|
||||
|
||||
// Re-export ideation types
|
||||
export type {
|
||||
Idea,
|
||||
IdeaCategory,
|
||||
IdeationSession,
|
||||
IdeationMessage,
|
||||
ProjectAnalysisResult,
|
||||
AnalysisSuggestion,
|
||||
StartSessionOptions,
|
||||
CreateIdeaInput,
|
||||
UpdateIdeaInput,
|
||||
ConvertToFeatureOptions,
|
||||
};
|
||||
|
||||
// Ideation API interface
|
||||
export interface IdeationAPI {
|
||||
// Session management
|
||||
startSession: (
|
||||
projectPath: string,
|
||||
options?: StartSessionOptions
|
||||
) => Promise<{ success: boolean; session?: IdeationSession; error?: string }>;
|
||||
getSession: (
|
||||
projectPath: string,
|
||||
sessionId: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
session?: IdeationSession;
|
||||
messages?: IdeationMessage[];
|
||||
error?: string;
|
||||
}>;
|
||||
sendMessage: (
|
||||
sessionId: string,
|
||||
message: string,
|
||||
options?: { imagePaths?: string[]; model?: string }
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
stopSession: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
|
||||
// Ideas CRUD
|
||||
listIdeas: (projectPath: string) => Promise<{ success: boolean; ideas?: Idea[]; error?: string }>;
|
||||
createIdea: (
|
||||
projectPath: string,
|
||||
idea: CreateIdeaInput
|
||||
) => Promise<{ success: boolean; idea?: Idea; error?: string }>;
|
||||
getIdea: (
|
||||
projectPath: string,
|
||||
ideaId: string
|
||||
) => Promise<{ success: boolean; idea?: Idea; error?: string }>;
|
||||
updateIdea: (
|
||||
projectPath: string,
|
||||
ideaId: string,
|
||||
updates: UpdateIdeaInput
|
||||
) => Promise<{ success: boolean; idea?: Idea; error?: string }>;
|
||||
deleteIdea: (
|
||||
projectPath: string,
|
||||
ideaId: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
|
||||
// Project analysis
|
||||
analyzeProject: (
|
||||
projectPath: string
|
||||
) => Promise<{ success: boolean; analysis?: ProjectAnalysisResult; error?: string }>;
|
||||
|
||||
// Generate suggestions from a prompt
|
||||
generateSuggestions: (
|
||||
projectPath: string,
|
||||
promptId: string,
|
||||
category: IdeaCategory,
|
||||
count?: number
|
||||
) => Promise<{ success: boolean; suggestions?: AnalysisSuggestion[]; error?: string }>;
|
||||
|
||||
// Convert to feature
|
||||
convertToFeature: (
|
||||
projectPath: string,
|
||||
ideaId: string,
|
||||
options?: ConvertToFeatureOptions
|
||||
) => Promise<{ success: boolean; feature?: any; featureId?: string; error?: string }>;
|
||||
|
||||
// Add suggestion directly to board as feature
|
||||
addSuggestionToBoard: (
|
||||
projectPath: string,
|
||||
suggestion: AnalysisSuggestion
|
||||
) => Promise<{ success: boolean; featureId?: string; error?: string }>;
|
||||
|
||||
// Event subscriptions
|
||||
onStream: (callback: (event: any) => void) => () => void;
|
||||
onAnalysisEvent: (callback: (event: any) => void) => () => void;
|
||||
}
|
||||
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
@@ -647,6 +745,7 @@ export interface ElectronAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
ideation?: IdeationAPI;
|
||||
}
|
||||
|
||||
// Note: Window interface is declared in @/types/electron.d.ts
|
||||
|
||||
@@ -26,6 +26,13 @@ import type {
|
||||
GitHubPR,
|
||||
IssueValidationInput,
|
||||
IssueValidationEvent,
|
||||
IdeationAPI,
|
||||
IdeaCategory,
|
||||
AnalysisSuggestion,
|
||||
StartSessionOptions,
|
||||
CreateIdeaInput,
|
||||
UpdateIdeaInput,
|
||||
ConvertToFeatureOptions,
|
||||
} from './electron';
|
||||
import type { Message, SessionListItem } from '@/types/electron';
|
||||
import type { Feature, ClaudeUsageResponse } from '@/store/app-store';
|
||||
@@ -368,7 +375,9 @@ type EventType =
|
||||
| 'suggestions:event'
|
||||
| 'spec-regeneration:event'
|
||||
| 'issue-validation:event'
|
||||
| 'backlog-plan:event';
|
||||
| 'backlog-plan:event'
|
||||
| 'ideation:stream'
|
||||
| 'ideation:analysis';
|
||||
|
||||
type EventCallback = (payload: unknown) => void;
|
||||
|
||||
@@ -1640,6 +1649,88 @@ export class HttpApiClient implements ElectronAPI {
|
||||
},
|
||||
};
|
||||
|
||||
// Ideation API - brainstorming and idea management
|
||||
ideation: IdeationAPI = {
|
||||
startSession: (projectPath: string, options?: StartSessionOptions) =>
|
||||
this.post('/api/ideation/session/start', { projectPath, options }),
|
||||
|
||||
getSession: (projectPath: string, sessionId: string) =>
|
||||
this.post('/api/ideation/session/get', { projectPath, sessionId }),
|
||||
|
||||
sendMessage: (
|
||||
sessionId: string,
|
||||
message: string,
|
||||
options?: { imagePaths?: string[]; model?: string }
|
||||
) => this.post('/api/ideation/session/message', { sessionId, message, options }),
|
||||
|
||||
stopSession: (sessionId: string) => this.post('/api/ideation/session/stop', { sessionId }),
|
||||
|
||||
listIdeas: (projectPath: string) => this.post('/api/ideation/ideas/list', { projectPath }),
|
||||
|
||||
createIdea: (projectPath: string, idea: CreateIdeaInput) =>
|
||||
this.post('/api/ideation/ideas/create', { projectPath, idea }),
|
||||
|
||||
getIdea: (projectPath: string, ideaId: string) =>
|
||||
this.post('/api/ideation/ideas/get', { projectPath, ideaId }),
|
||||
|
||||
updateIdea: (projectPath: string, ideaId: string, updates: UpdateIdeaInput) =>
|
||||
this.post('/api/ideation/ideas/update', { projectPath, ideaId, updates }),
|
||||
|
||||
deleteIdea: (projectPath: string, ideaId: string) =>
|
||||
this.post('/api/ideation/ideas/delete', { projectPath, ideaId }),
|
||||
|
||||
analyzeProject: (projectPath: string) => this.post('/api/ideation/analyze', { projectPath }),
|
||||
|
||||
generateSuggestions: (
|
||||
projectPath: string,
|
||||
promptId: string,
|
||||
category: IdeaCategory,
|
||||
count?: number
|
||||
) =>
|
||||
this.post('/api/ideation/suggestions/generate', { projectPath, promptId, category, count }),
|
||||
|
||||
convertToFeature: (projectPath: string, ideaId: string, options?: ConvertToFeatureOptions) =>
|
||||
this.post('/api/ideation/convert', { projectPath, ideaId, ...options }),
|
||||
|
||||
addSuggestionToBoard: async (projectPath: string, suggestion: AnalysisSuggestion) => {
|
||||
// Create a feature directly from the suggestion
|
||||
const result = await this.post<{ success: boolean; feature?: Feature; error?: string }>(
|
||||
'/api/features/create',
|
||||
{
|
||||
projectPath,
|
||||
feature: {
|
||||
title: suggestion.title,
|
||||
description:
|
||||
suggestion.description +
|
||||
(suggestion.rationale ? `\n\n**Rationale:** ${suggestion.rationale}` : ''),
|
||||
category:
|
||||
suggestion.category === 'ux-ui'
|
||||
? 'enhancement'
|
||||
: suggestion.category === 'dx'
|
||||
? 'chore'
|
||||
: suggestion.category === 'technical'
|
||||
? 'refactor'
|
||||
: 'feature',
|
||||
status: 'backlog',
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
success: result.success,
|
||||
featureId: result.feature?.id,
|
||||
error: result.error,
|
||||
};
|
||||
},
|
||||
|
||||
onStream: (callback: (event: any) => void): (() => void) => {
|
||||
return this.subscribeToEvent('ideation:stream', callback as EventCallback);
|
||||
},
|
||||
|
||||
onAnalysisEvent: (callback: (event: any) => void): (() => void) => {
|
||||
return this.subscribeToEvent('ideation:analysis', callback as EventCallback);
|
||||
},
|
||||
};
|
||||
|
||||
// MCP API - Test MCP server connections and list tools
|
||||
// SECURITY: Only accepts serverId, not arbitrary serverConfig, to prevent
|
||||
// drive-by command execution attacks. Servers must be saved first.
|
||||
|
||||
6
apps/ui/src/routes/ideation.tsx
Normal file
6
apps/ui/src/routes/ideation.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { IdeationView } from '@/components/views/ideation-view';
|
||||
|
||||
export const Route = createFileRoute('/ideation')({
|
||||
component: IdeationView,
|
||||
});
|
||||
@@ -29,7 +29,8 @@ export type ViewMode =
|
||||
| 'profiles'
|
||||
| 'running-agents'
|
||||
| 'terminal'
|
||||
| 'wiki';
|
||||
| 'wiki'
|
||||
| 'ideation';
|
||||
|
||||
export type ThemeMode =
|
||||
| 'light'
|
||||
@@ -154,6 +155,9 @@ export interface KeyboardShortcuts {
|
||||
settings: string;
|
||||
profiles: string;
|
||||
terminal: string;
|
||||
ideation: string;
|
||||
githubIssues: string;
|
||||
githubPrs: string;
|
||||
|
||||
// UI shortcuts
|
||||
toggleSidebar: string;
|
||||
@@ -186,6 +190,9 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
|
||||
settings: 'S',
|
||||
profiles: 'M',
|
||||
terminal: 'T',
|
||||
ideation: 'I',
|
||||
githubIssues: 'G',
|
||||
githubPrs: 'R',
|
||||
|
||||
// UI
|
||||
toggleSidebar: '`',
|
||||
|
||||
324
apps/ui/src/store/ideation-store.ts
Normal file
324
apps/ui/src/store/ideation-store.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* Ideation Store - State management for brainstorming and idea management
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type {
|
||||
Idea,
|
||||
IdeaCategory,
|
||||
IdeaStatus,
|
||||
IdeationPrompt,
|
||||
AnalysisSuggestion,
|
||||
ProjectAnalysisResult,
|
||||
} from '@automaker/types';
|
||||
|
||||
// ============================================================================
|
||||
// Generation Job Types
|
||||
// ============================================================================
|
||||
|
||||
export type GenerationJobStatus = 'generating' | 'ready' | 'error';
|
||||
|
||||
export interface GenerationJob {
|
||||
id: string;
|
||||
prompt: IdeationPrompt;
|
||||
status: GenerationJobStatus;
|
||||
suggestions: AnalysisSuggestion[];
|
||||
error: string | null;
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State Interface
|
||||
// ============================================================================
|
||||
|
||||
export type IdeationMode = 'dashboard' | 'prompts';
|
||||
|
||||
interface IdeationState {
|
||||
// Ideas (saved for later)
|
||||
ideas: Idea[];
|
||||
selectedIdeaId: string | null;
|
||||
|
||||
// Generation jobs (multiple concurrent generations)
|
||||
generationJobs: GenerationJob[];
|
||||
selectedJobId: string | null;
|
||||
|
||||
// Legacy - keep for backwards compat during transition
|
||||
suggestions: AnalysisSuggestion[];
|
||||
selectedPrompt: IdeationPrompt | null;
|
||||
isGenerating: boolean;
|
||||
generatingError: string | null;
|
||||
|
||||
// Analysis
|
||||
analysisResult: ProjectAnalysisResult | null;
|
||||
isAnalyzing: boolean;
|
||||
analysisProgress: number;
|
||||
analysisMessage: string;
|
||||
|
||||
// UI state
|
||||
currentMode: IdeationMode;
|
||||
selectedCategory: IdeaCategory | null;
|
||||
filterStatus: IdeaStatus | 'all';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Actions Interface
|
||||
// ============================================================================
|
||||
|
||||
interface IdeationActions {
|
||||
// Ideas
|
||||
setIdeas: (ideas: Idea[]) => void;
|
||||
addIdea: (idea: Idea) => void;
|
||||
updateIdea: (id: string, updates: Partial<Idea>) => void;
|
||||
removeIdea: (id: string) => void;
|
||||
setSelectedIdea: (id: string | null) => void;
|
||||
getSelectedIdea: () => Idea | null;
|
||||
|
||||
// Generation Jobs
|
||||
addGenerationJob: (prompt: IdeationPrompt) => string;
|
||||
updateJobStatus: (
|
||||
jobId: string,
|
||||
status: GenerationJobStatus,
|
||||
suggestions?: AnalysisSuggestion[],
|
||||
error?: string
|
||||
) => void;
|
||||
removeJob: (jobId: string) => void;
|
||||
clearCompletedJobs: () => void;
|
||||
setSelectedJob: (jobId: string | null) => void;
|
||||
getJob: (jobId: string) => GenerationJob | null;
|
||||
removeSuggestionFromJob: (jobId: string, suggestionId: string) => void;
|
||||
appendSuggestionsToJob: (jobId: string, suggestions: AnalysisSuggestion[]) => void;
|
||||
setJobGenerating: (jobId: string, generating: boolean) => void;
|
||||
|
||||
// Legacy Suggestions (kept for backwards compat)
|
||||
setSuggestions: (suggestions: AnalysisSuggestion[]) => void;
|
||||
clearSuggestions: () => void;
|
||||
removeSuggestion: (id: string) => void;
|
||||
setSelectedPrompt: (prompt: IdeationPrompt | null) => void;
|
||||
setIsGenerating: (isGenerating: boolean) => void;
|
||||
setGeneratingError: (error: string | null) => void;
|
||||
|
||||
// Analysis
|
||||
setAnalysisResult: (result: ProjectAnalysisResult | null) => void;
|
||||
setIsAnalyzing: (isAnalyzing: boolean) => void;
|
||||
setAnalysisProgress: (progress: number, message?: string) => void;
|
||||
|
||||
// UI
|
||||
setMode: (mode: IdeationMode) => void;
|
||||
setCategory: (category: IdeaCategory | null) => void;
|
||||
setFilterStatus: (status: IdeaStatus | 'all') => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
resetSuggestions: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Initial State
|
||||
// ============================================================================
|
||||
|
||||
const initialState: IdeationState = {
|
||||
ideas: [],
|
||||
selectedIdeaId: null,
|
||||
generationJobs: [],
|
||||
selectedJobId: null,
|
||||
suggestions: [],
|
||||
selectedPrompt: null,
|
||||
isGenerating: false,
|
||||
generatingError: null,
|
||||
analysisResult: null,
|
||||
isAnalyzing: false,
|
||||
analysisProgress: 0,
|
||||
analysisMessage: '',
|
||||
currentMode: 'dashboard',
|
||||
selectedCategory: null,
|
||||
filterStatus: 'all',
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Store
|
||||
// ============================================================================
|
||||
|
||||
export const useIdeationStore = create<IdeationState & IdeationActions>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
|
||||
// Ideas
|
||||
setIdeas: (ideas) => set({ ideas }),
|
||||
|
||||
addIdea: (idea) =>
|
||||
set((state) => ({
|
||||
ideas: [idea, ...state.ideas],
|
||||
})),
|
||||
|
||||
updateIdea: (id, updates) =>
|
||||
set((state) => ({
|
||||
ideas: state.ideas.map((idea) => (idea.id === id ? { ...idea, ...updates } : idea)),
|
||||
})),
|
||||
|
||||
removeIdea: (id) =>
|
||||
set((state) => ({
|
||||
ideas: state.ideas.filter((idea) => idea.id !== id),
|
||||
selectedIdeaId: state.selectedIdeaId === id ? null : state.selectedIdeaId,
|
||||
})),
|
||||
|
||||
setSelectedIdea: (id) => set({ selectedIdeaId: id }),
|
||||
|
||||
getSelectedIdea: () => {
|
||||
const state = get();
|
||||
return state.ideas.find((idea) => idea.id === state.selectedIdeaId) || null;
|
||||
},
|
||||
|
||||
// Generation Jobs
|
||||
addGenerationJob: (prompt) => {
|
||||
const jobId = `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const job: GenerationJob = {
|
||||
id: jobId,
|
||||
prompt,
|
||||
status: 'generating',
|
||||
suggestions: [],
|
||||
error: null,
|
||||
startedAt: new Date().toISOString(),
|
||||
completedAt: null,
|
||||
};
|
||||
set((state) => ({
|
||||
generationJobs: [job, ...state.generationJobs],
|
||||
}));
|
||||
return jobId;
|
||||
},
|
||||
|
||||
updateJobStatus: (jobId, status, suggestions, error) =>
|
||||
set((state) => ({
|
||||
generationJobs: state.generationJobs.map((job) =>
|
||||
job.id === jobId
|
||||
? {
|
||||
...job,
|
||||
status,
|
||||
suggestions: suggestions || job.suggestions,
|
||||
error: error || null,
|
||||
completedAt: status !== 'generating' ? new Date().toISOString() : null,
|
||||
}
|
||||
: job
|
||||
),
|
||||
})),
|
||||
|
||||
removeJob: (jobId) =>
|
||||
set((state) => ({
|
||||
generationJobs: state.generationJobs.filter((job) => job.id !== jobId),
|
||||
selectedJobId: state.selectedJobId === jobId ? null : state.selectedJobId,
|
||||
})),
|
||||
|
||||
clearCompletedJobs: () =>
|
||||
set((state) => ({
|
||||
generationJobs: state.generationJobs.filter((job) => job.status === 'generating'),
|
||||
})),
|
||||
|
||||
setSelectedJob: (jobId) => set({ selectedJobId: jobId }),
|
||||
|
||||
getJob: (jobId) => {
|
||||
const state = get();
|
||||
return state.generationJobs.find((job) => job.id === jobId) || null;
|
||||
},
|
||||
|
||||
removeSuggestionFromJob: (jobId, suggestionId) =>
|
||||
set((state) => ({
|
||||
generationJobs: state.generationJobs.map((job) =>
|
||||
job.id === jobId
|
||||
? {
|
||||
...job,
|
||||
suggestions: job.suggestions.filter((s) => s.id !== suggestionId),
|
||||
}
|
||||
: job
|
||||
),
|
||||
})),
|
||||
|
||||
appendSuggestionsToJob: (jobId, suggestions) =>
|
||||
set((state) => ({
|
||||
generationJobs: state.generationJobs.map((job) =>
|
||||
job.id === jobId
|
||||
? {
|
||||
...job,
|
||||
suggestions: [...job.suggestions, ...suggestions],
|
||||
status: 'ready' as const,
|
||||
}
|
||||
: job
|
||||
),
|
||||
})),
|
||||
|
||||
setJobGenerating: (jobId, generating) =>
|
||||
set((state) => ({
|
||||
generationJobs: state.generationJobs.map((job) =>
|
||||
job.id === jobId
|
||||
? {
|
||||
...job,
|
||||
status: generating ? ('generating' as const) : ('ready' as const),
|
||||
}
|
||||
: job
|
||||
),
|
||||
})),
|
||||
|
||||
// Suggestions (legacy)
|
||||
setSuggestions: (suggestions) => set({ suggestions }),
|
||||
|
||||
clearSuggestions: () => set({ suggestions: [], generatingError: null }),
|
||||
|
||||
removeSuggestion: (id) =>
|
||||
set((state) => ({
|
||||
suggestions: state.suggestions.filter((s) => s.id !== id),
|
||||
})),
|
||||
|
||||
setSelectedPrompt: (prompt) => set({ selectedPrompt: prompt }),
|
||||
|
||||
setIsGenerating: (isGenerating) => set({ isGenerating }),
|
||||
|
||||
setGeneratingError: (error) => set({ generatingError: error }),
|
||||
|
||||
// Analysis
|
||||
setAnalysisResult: (result) => set({ analysisResult: result }),
|
||||
|
||||
setIsAnalyzing: (isAnalyzing) =>
|
||||
set({
|
||||
isAnalyzing,
|
||||
analysisProgress: isAnalyzing ? 0 : get().analysisProgress,
|
||||
analysisMessage: isAnalyzing ? 'Starting analysis...' : '',
|
||||
}),
|
||||
|
||||
setAnalysisProgress: (progress, message) =>
|
||||
set({
|
||||
analysisProgress: progress,
|
||||
analysisMessage: message || get().analysisMessage,
|
||||
}),
|
||||
|
||||
// UI
|
||||
setMode: (mode) => set({ currentMode: mode }),
|
||||
|
||||
setCategory: (category) => set({ selectedCategory: category }),
|
||||
|
||||
setFilterStatus: (status) => set({ filterStatus: status }),
|
||||
|
||||
// Reset
|
||||
reset: () => set(initialState),
|
||||
|
||||
resetSuggestions: () =>
|
||||
set({
|
||||
suggestions: [],
|
||||
selectedPrompt: null,
|
||||
isGenerating: false,
|
||||
generatingError: null,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'automaker-ideation-store',
|
||||
version: 3,
|
||||
partialize: (state) => ({
|
||||
// Only persist these fields
|
||||
ideas: state.ideas,
|
||||
generationJobs: state.generationJobs,
|
||||
analysisResult: state.analysisResult,
|
||||
filterStatus: state.filterStatus,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -23,6 +23,17 @@ export {
|
||||
getCredentialsPath,
|
||||
getProjectSettingsPath,
|
||||
ensureDataDir,
|
||||
// Ideation paths
|
||||
getIdeationDir,
|
||||
getIdeasDir,
|
||||
getIdeaDir,
|
||||
getIdeaPath,
|
||||
getIdeaAttachmentsDir,
|
||||
getIdeationSessionsDir,
|
||||
getIdeationSessionPath,
|
||||
getIdeationDraftsDir,
|
||||
getIdeationAnalysisPath,
|
||||
ensureIdeationDir,
|
||||
} from './paths.js';
|
||||
|
||||
// Subprocess management
|
||||
|
||||
@@ -188,6 +188,140 @@ export async function ensureAutomakerDir(projectPath: string): Promise<string> {
|
||||
return automakerDir;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ideation Paths
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get the ideation directory for a project
|
||||
*
|
||||
* Contains ideas, sessions, and drafts for brainstorming.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation
|
||||
*/
|
||||
export function getIdeationDir(projectPath: string): string {
|
||||
return path.join(getAutomakerDir(projectPath), 'ideation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ideas directory for a project
|
||||
*
|
||||
* Contains subdirectories for each idea, keyed by ideaId.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/ideas
|
||||
*/
|
||||
export function getIdeasDir(projectPath: string): string {
|
||||
return path.join(getIdeationDir(projectPath), 'ideas');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory for a specific idea
|
||||
*
|
||||
* Contains idea metadata and attachments.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @param ideaId - Idea identifier
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/ideas/{ideaId}
|
||||
*/
|
||||
export function getIdeaDir(projectPath: string, ideaId: string): string {
|
||||
return path.join(getIdeasDir(projectPath), ideaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the idea metadata file path
|
||||
*
|
||||
* Stores the idea JSON data.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @param ideaId - Idea identifier
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/ideas/{ideaId}/idea.json
|
||||
*/
|
||||
export function getIdeaPath(projectPath: string, ideaId: string): string {
|
||||
return path.join(getIdeaDir(projectPath, ideaId), 'idea.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the idea attachments directory
|
||||
*
|
||||
* Stores images and other attachments for an idea.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @param ideaId - Idea identifier
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/ideas/{ideaId}/attachments
|
||||
*/
|
||||
export function getIdeaAttachmentsDir(projectPath: string, ideaId: string): string {
|
||||
return path.join(getIdeaDir(projectPath, ideaId), 'attachments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ideation sessions directory for a project
|
||||
*
|
||||
* Contains conversation history for ideation sessions.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/sessions
|
||||
*/
|
||||
export function getIdeationSessionsDir(projectPath: string): string {
|
||||
return path.join(getIdeationDir(projectPath), 'sessions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session file path for an ideation session
|
||||
*
|
||||
* Stores the session messages and metadata.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @param sessionId - Session identifier
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/sessions/{sessionId}.json
|
||||
*/
|
||||
export function getIdeationSessionPath(projectPath: string, sessionId: string): string {
|
||||
return path.join(getIdeationSessionsDir(projectPath), `${sessionId}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ideation drafts directory for a project
|
||||
*
|
||||
* Stores unsaved conversation drafts.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/drafts
|
||||
*/
|
||||
export function getIdeationDraftsDir(projectPath: string): string {
|
||||
return path.join(getIdeationDir(projectPath), 'drafts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project analysis result file path
|
||||
*
|
||||
* Stores the cached project analysis result.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @returns Absolute path to {projectPath}/.automaker/ideation/analysis.json
|
||||
*/
|
||||
export function getIdeationAnalysisPath(projectPath: string): string {
|
||||
return path.join(getIdeationDir(projectPath), 'analysis.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the ideation directory structure for a project if it doesn't exist
|
||||
*
|
||||
* Creates {projectPath}/.automaker/ideation with all subdirectories.
|
||||
* Safe to call multiple times - uses recursive: true.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @returns Promise resolving to the created ideation directory path
|
||||
*/
|
||||
export async function ensureIdeationDir(projectPath: string): Promise<string> {
|
||||
const ideationDir = getIdeationDir(projectPath);
|
||||
await secureFs.mkdir(ideationDir, { recursive: true });
|
||||
await secureFs.mkdir(getIdeasDir(projectPath), { recursive: true });
|
||||
await secureFs.mkdir(getIdeationSessionsDir(projectPath), { recursive: true });
|
||||
await secureFs.mkdir(getIdeationDraftsDir(projectPath), { recursive: true });
|
||||
return ideationDir;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global Settings Paths (stored in DATA_DIR from app.getPath('userData'))
|
||||
// ============================================================================
|
||||
|
||||
@@ -26,6 +26,15 @@ export type EventType =
|
||||
| 'project:analysis-error'
|
||||
| 'suggestions:event'
|
||||
| 'spec-regeneration:event'
|
||||
| 'issue-validation:event';
|
||||
| 'issue-validation:event'
|
||||
| 'ideation:stream'
|
||||
| 'ideation:session-started'
|
||||
| 'ideation:session-ended'
|
||||
| 'ideation:analysis'
|
||||
| 'ideation:analysis-started'
|
||||
| 'ideation:analysis-progress'
|
||||
| 'ideation:analysis-complete'
|
||||
| 'ideation:analysis-error'
|
||||
| 'ideation:suggestions';
|
||||
|
||||
export type EventCallback = (type: EventType, payload: unknown) => void;
|
||||
|
||||
230
libs/types/src/ideation.ts
Normal file
230
libs/types/src/ideation.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Ideation types for AutoMaker brainstorming and idea management
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Core Types
|
||||
// ============================================================================
|
||||
|
||||
export type IdeaCategory =
|
||||
| 'feature'
|
||||
| 'ux-ui'
|
||||
| 'dx'
|
||||
| 'growth'
|
||||
| 'technical'
|
||||
| 'security'
|
||||
| 'performance'
|
||||
| 'accessibility'
|
||||
| 'analytics';
|
||||
export type IdeaStatus = 'raw' | 'refined' | 'ready' | 'archived';
|
||||
export type ImpactLevel = 'low' | 'medium' | 'high';
|
||||
export type EffortLevel = 'low' | 'medium' | 'high';
|
||||
|
||||
// ============================================================================
|
||||
// Idea Entity
|
||||
// ============================================================================
|
||||
|
||||
export interface IdeaAttachment {
|
||||
id: string;
|
||||
type: 'image' | 'link' | 'reference';
|
||||
path?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Idea {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: IdeaCategory;
|
||||
status: IdeaStatus;
|
||||
impact: ImpactLevel;
|
||||
effort: EffortLevel;
|
||||
|
||||
// Conversation context
|
||||
conversationId?: string;
|
||||
sourcePromptId?: string;
|
||||
|
||||
// Content
|
||||
attachments?: IdeaAttachment[];
|
||||
userStories?: string[];
|
||||
notes?: string;
|
||||
|
||||
// Metadata
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
archivedAt?: string;
|
||||
|
||||
// Extensibility
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ideation Session
|
||||
// ============================================================================
|
||||
|
||||
export type IdeationSessionStatus = 'active' | 'completed' | 'abandoned';
|
||||
|
||||
export interface IdeationSession {
|
||||
id: string;
|
||||
projectPath: string;
|
||||
promptCategory?: IdeaCategory;
|
||||
promptId?: string;
|
||||
status: IdeationSessionStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface IdeationMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: string;
|
||||
savedAsIdeaId?: string;
|
||||
}
|
||||
|
||||
export interface IdeationSessionWithMessages extends IdeationSession {
|
||||
messages: IdeationMessage[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Guided Prompts
|
||||
// ============================================================================
|
||||
|
||||
export interface PromptCategory {
|
||||
id: IdeaCategory;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface IdeationPrompt {
|
||||
id: string;
|
||||
category: IdeaCategory;
|
||||
title: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Project Analysis
|
||||
// ============================================================================
|
||||
|
||||
export interface AnalysisFileInfo {
|
||||
path: string;
|
||||
type: 'route' | 'component' | 'service' | 'model' | 'config' | 'test' | 'other';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AnalysisSuggestion {
|
||||
id: string;
|
||||
category: IdeaCategory;
|
||||
title: string;
|
||||
description: string;
|
||||
rationale: string;
|
||||
relatedFiles?: string[];
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
export interface ProjectAnalysisResult {
|
||||
projectPath: string;
|
||||
analyzedAt: string;
|
||||
|
||||
// Structure analysis
|
||||
totalFiles: number;
|
||||
routes: AnalysisFileInfo[];
|
||||
components: AnalysisFileInfo[];
|
||||
services: AnalysisFileInfo[];
|
||||
|
||||
// Tech stack detection
|
||||
framework?: string;
|
||||
language?: string;
|
||||
dependencies?: string[];
|
||||
|
||||
// AI-generated suggestions
|
||||
suggestions: AnalysisSuggestion[];
|
||||
|
||||
// Summary
|
||||
summary: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Types
|
||||
// ============================================================================
|
||||
|
||||
export interface StartSessionOptions {
|
||||
promptId?: string;
|
||||
promptCategory?: IdeaCategory;
|
||||
initialMessage?: string;
|
||||
}
|
||||
|
||||
export interface SendMessageOptions {
|
||||
imagePaths?: string[];
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface CreateIdeaInput {
|
||||
title: string;
|
||||
description: string;
|
||||
category: IdeaCategory;
|
||||
status?: IdeaStatus;
|
||||
impact?: ImpactLevel;
|
||||
effort?: EffortLevel;
|
||||
conversationId?: string;
|
||||
sourcePromptId?: string;
|
||||
userStories?: string[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateIdeaInput {
|
||||
title?: string;
|
||||
description?: string;
|
||||
category?: IdeaCategory;
|
||||
status?: IdeaStatus;
|
||||
impact?: ImpactLevel;
|
||||
effort?: EffortLevel;
|
||||
userStories?: string[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ConvertToFeatureOptions {
|
||||
column?: string;
|
||||
dependencies?: string[];
|
||||
tags?: string[];
|
||||
keepIdea?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event Types
|
||||
// ============================================================================
|
||||
|
||||
export type IdeationEventType =
|
||||
| 'ideation:stream'
|
||||
| 'ideation:session-started'
|
||||
| 'ideation:session-ended'
|
||||
| 'ideation:analysis-started'
|
||||
| 'ideation:analysis-progress'
|
||||
| 'ideation:analysis-complete'
|
||||
| 'ideation:analysis-error';
|
||||
|
||||
export interface IdeationStreamEvent {
|
||||
type: 'ideation:stream';
|
||||
sessionId: string;
|
||||
content: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export interface IdeationAnalysisEvent {
|
||||
type:
|
||||
| 'ideation:analysis-started'
|
||||
| 'ideation:analysis-progress'
|
||||
| 'ideation:analysis-complete'
|
||||
| 'ideation:analysis-error';
|
||||
projectPath: string;
|
||||
progress?: number;
|
||||
message?: string;
|
||||
result?: ProjectAnalysisResult;
|
||||
error?: string;
|
||||
}
|
||||
@@ -143,3 +143,30 @@ export type {
|
||||
|
||||
// Port configuration
|
||||
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './ports.js';
|
||||
|
||||
// Ideation types
|
||||
export type {
|
||||
IdeaCategory,
|
||||
IdeaStatus,
|
||||
ImpactLevel,
|
||||
EffortLevel,
|
||||
IdeaAttachment,
|
||||
Idea,
|
||||
IdeationSessionStatus,
|
||||
IdeationSession,
|
||||
IdeationMessage,
|
||||
IdeationSessionWithMessages,
|
||||
PromptCategory,
|
||||
IdeationPrompt,
|
||||
AnalysisFileInfo,
|
||||
AnalysisSuggestion,
|
||||
ProjectAnalysisResult,
|
||||
StartSessionOptions,
|
||||
SendMessageOptions,
|
||||
CreateIdeaInput,
|
||||
UpdateIdeaInput,
|
||||
ConvertToFeatureOptions,
|
||||
IdeationEventType,
|
||||
IdeationStreamEvent,
|
||||
IdeationAnalysisEvent,
|
||||
} from './ideation.js';
|
||||
|
||||
Reference in New Issue
Block a user