From ddf47ae5ecc75f34fd597c41abce900607c16702 Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Tue, 16 Dec 2025 15:02:16 +0100 Subject: [PATCH] Roadmap competitor analysis --- .../src/main/agent/agent-manager.ts | 5 +- auto-claude-ui/src/main/agent/agent-queue.ts | 8 +- .../src/main/ipc-handlers/roadmap-handlers.ts | 73 +++- .../components/CompetitorAnalysisDialog.tsx | 110 +++++ .../src/renderer/components/Roadmap.tsx | 183 ++++++-- .../src/renderer/stores/roadmap-store.ts | 29 +- auto-claude-ui/src/shared/constants.ts | 2 + auto-claude-ui/src/shared/types/roadmap.ts | 63 +++ auto-claude/prompts/competitor_analysis.md | 405 ++++++++++++++++++ auto-claude/prompts/roadmap_discovery.md | 39 +- auto-claude/prompts/roadmap_features.md | 53 ++- auto-claude/roadmap_runner.py | 141 +++++- 12 files changed, 1037 insertions(+), 74 deletions(-) create mode 100644 auto-claude-ui/src/renderer/components/CompetitorAnalysisDialog.tsx create mode 100644 auto-claude/prompts/competitor_analysis.md diff --git a/auto-claude-ui/src/main/agent/agent-manager.ts b/auto-claude-ui/src/main/agent/agent-manager.ts index 63a0c91c..59634db8 100644 --- a/auto-claude-ui/src/main/agent/agent-manager.ts +++ b/auto-claude-ui/src/main/agent/agent-manager.ts @@ -168,9 +168,10 @@ export class AgentManager extends EventEmitter { startRoadmapGeneration( projectId: string, projectPath: string, - refresh: boolean = false + refresh: boolean = false, + enableCompetitorAnalysis: boolean = false ): void { - this.queueManager.startRoadmapGeneration(projectId, projectPath, refresh); + this.queueManager.startRoadmapGeneration(projectId, projectPath, refresh, enableCompetitorAnalysis); } /** diff --git a/auto-claude-ui/src/main/agent/agent-queue.ts b/auto-claude-ui/src/main/agent/agent-queue.ts index ef9d536f..167151a5 100644 --- a/auto-claude-ui/src/main/agent/agent-queue.ts +++ b/auto-claude-ui/src/main/agent/agent-queue.ts @@ -35,7 +35,8 @@ export class AgentQueueManager { startRoadmapGeneration( projectId: string, projectPath: string, - refresh: boolean = false + refresh: boolean = false, + enableCompetitorAnalysis: boolean = false ): void { const autoBuildSource = this.processManager.getAutoBuildSourcePath(); @@ -57,6 +58,11 @@ export class AgentQueueManager { args.push('--refresh'); } + // Add competitor analysis flag if enabled + if (enableCompetitorAnalysis) { + args.push('--competitor-analysis'); + } + // Use projectId as taskId for roadmap operations this.spawnRoadmapProcess(projectId, projectPath, args); } diff --git a/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts index 68d40de9..316a6e19 100644 --- a/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts @@ -1,7 +1,7 @@ import { ipcMain } from 'electron'; import type { BrowserWindow } from 'electron'; import { IPC_CHANNELS, AUTO_BUILD_PATHS, getSpecsDir } from '../../shared/constants'; -import type { IPCResult, Roadmap, RoadmapFeature, RoadmapFeatureStatus, RoadmapGenerationStatus, Task, TaskMetadata } from '../../shared/types'; +import type { IPCResult, Roadmap, RoadmapFeature, RoadmapFeatureStatus, RoadmapGenerationStatus, Task, TaskMetadata, CompetitorAnalysis } from '../../shared/types'; import path from 'path'; import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'; import { projectStore } from '../project-store'; @@ -42,6 +42,65 @@ export function registerRoadmapHandlers( const content = readFileSync(roadmapPath, 'utf-8'); const rawRoadmap = JSON.parse(content); + // Load competitor analysis if available (competitor_analysis.json) + const competitorAnalysisPath = path.join( + project.path, + AUTO_BUILD_PATHS.ROADMAP_DIR, + AUTO_BUILD_PATHS.COMPETITOR_ANALYSIS + ); + let competitorAnalysis: CompetitorAnalysis | undefined; + if (existsSync(competitorAnalysisPath)) { + try { + const competitorContent = readFileSync(competitorAnalysisPath, 'utf-8'); + const rawCompetitor = JSON.parse(competitorContent); + // Transform snake_case to camelCase for frontend + competitorAnalysis = { + projectContext: { + projectName: rawCompetitor.project_context?.project_name || '', + projectType: rawCompetitor.project_context?.project_type || '', + targetAudience: rawCompetitor.project_context?.target_audience || '' + }, + competitors: (rawCompetitor.competitors || []).map((c: Record) => ({ + id: c.id, + name: c.name, + url: c.url, + description: c.description, + relevance: c.relevance || 'medium', + painPoints: ((c.pain_points as Array>) || []).map((p) => ({ + id: p.id, + description: p.description, + source: p.source, + severity: p.severity || 'medium', + frequency: p.frequency || '', + opportunity: p.opportunity || '' + })), + strengths: (c.strengths as string[]) || [], + marketPosition: (c.market_position as string) || '' + })), + marketGaps: (rawCompetitor.market_gaps || []).map((g: Record) => ({ + id: g.id, + description: g.description, + affectedCompetitors: (g.affected_competitors as string[]) || [], + opportunitySize: g.opportunity_size || 'medium', + suggestedFeature: (g.suggested_feature as string) || '' + })), + insightsSummary: { + topPainPoints: rawCompetitor.insights_summary?.top_pain_points || [], + differentiatorOpportunities: rawCompetitor.insights_summary?.differentiator_opportunities || [], + marketTrends: rawCompetitor.insights_summary?.market_trends || [] + }, + researchMetadata: { + searchQueriesUsed: rawCompetitor.research_metadata?.search_queries_used || [], + sourcesConsulted: rawCompetitor.research_metadata?.sources_consulted || [], + limitations: rawCompetitor.research_metadata?.limitations || [] + }, + createdAt: rawCompetitor.metadata?.created_at ? new Date(rawCompetitor.metadata.created_at) : new Date() + }; + } catch { + // Ignore competitor analysis parsing errors - it's optional + } + } + // Transform snake_case to camelCase for frontend const roadmap: Roadmap = { id: rawRoadmap.id || `roadmap-${Date.now()}`, @@ -82,9 +141,11 @@ export function registerRoadmapHandlers( status: feature.status || 'idea', acceptanceCriteria: feature.acceptance_criteria || [], userStories: feature.user_stories || [], - linkedSpecId: feature.linked_spec_id + linkedSpecId: feature.linked_spec_id, + competitorInsightIds: (feature.competitor_insight_ids as string[]) || undefined })), status: rawRoadmap.status || 'draft', + competitorAnalysis, createdAt: rawRoadmap.metadata?.created_at ? new Date(rawRoadmap.metadata.created_at) : new Date(), updatedAt: rawRoadmap.metadata?.updated_at ? new Date(rawRoadmap.metadata.updated_at) : new Date() }; @@ -101,7 +162,7 @@ export function registerRoadmapHandlers( ipcMain.on( IPC_CHANNELS.ROADMAP_GENERATE, - (_, projectId: string) => { + (_, projectId: string, enableCompetitorAnalysis?: boolean) => { const mainWindow = getMainWindow(); if (!mainWindow) return; @@ -116,7 +177,7 @@ export function registerRoadmapHandlers( } // Start roadmap generation via agent manager - agentManager.startRoadmapGeneration(projectId, project.path, false); + agentManager.startRoadmapGeneration(projectId, project.path, false, enableCompetitorAnalysis ?? false); // Send initial progress mainWindow.webContents.send( @@ -133,7 +194,7 @@ export function registerRoadmapHandlers( ipcMain.on( IPC_CHANNELS.ROADMAP_REFRESH, - (_, projectId: string) => { + (_, projectId: string, enableCompetitorAnalysis?: boolean) => { const mainWindow = getMainWindow(); if (!mainWindow) return; @@ -148,7 +209,7 @@ export function registerRoadmapHandlers( } // Start roadmap regeneration with refresh flag - agentManager.startRoadmapGeneration(projectId, project.path, true); + agentManager.startRoadmapGeneration(projectId, project.path, true, enableCompetitorAnalysis ?? false); // Send initial progress mainWindow.webContents.send( diff --git a/auto-claude-ui/src/renderer/components/CompetitorAnalysisDialog.tsx b/auto-claude-ui/src/renderer/components/CompetitorAnalysisDialog.tsx new file mode 100644 index 00000000..86b6ef21 --- /dev/null +++ b/auto-claude-ui/src/renderer/components/CompetitorAnalysisDialog.tsx @@ -0,0 +1,110 @@ +import { Search, Globe, AlertTriangle, TrendingUp } from 'lucide-react'; +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogAction, + AlertDialogCancel, +} from './ui/alert-dialog'; + +interface CompetitorAnalysisDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onAccept: () => void; + onDecline: () => void; +} + +export function CompetitorAnalysisDialog({ + open, + onOpenChange, + onAccept, + onDecline, +}: CompetitorAnalysisDialogProps) { + const handleAccept = () => { + onAccept(); + onOpenChange(false); + }; + + const handleDecline = () => { + onDecline(); + onOpenChange(false); + }; + + return ( + + + + + + Enable Competitor Analysis? + + + Enhance your roadmap with insights from competitor products + + + +
+ {/* What it does */} +
+

+ What competitor analysis does: +

+
    +
  • + + Identifies 3-5 main competitors based on your project type +
  • +
  • + + + Searches app stores, forums, and social media for user feedback and pain points + +
  • +
  • + + + Suggests features that address gaps in competitor products + +
  • +
+
+ + {/* Privacy notice */} +
+
+ +
+

+ Web searches will be performed +

+

+ This feature will perform web searches to gather competitor information. + Your project name and type will be used in search queries. + No code or sensitive data is shared. +

+
+
+
+ + {/* Optional info */} +

+ You can generate a roadmap without competitor analysis if you prefer. + The roadmap will still be based on your project structure and best practices. +

+
+ + + + No, Skip Analysis + + + Yes, Enable Analysis + + +
+
+ ); +} diff --git a/auto-claude-ui/src/renderer/components/Roadmap.tsx b/auto-claude-ui/src/renderer/components/Roadmap.tsx index 5cc9ce82..9929305e 100644 --- a/auto-claude-ui/src/renderer/components/Roadmap.tsx +++ b/auto-claude-ui/src/renderer/components/Roadmap.tsx @@ -15,8 +15,8 @@ import { Clock, AlertCircle, Play, - ExternalLink -} from 'lucide-react'; + ExternalLink, + TrendingUp} from 'lucide-react'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { Card } from './ui/card'; @@ -41,7 +41,8 @@ import { ROADMAP_COMPLEXITY_COLORS, ROADMAP_IMPACT_COLORS } from '../../shared/constants'; -import type { RoadmapFeature, RoadmapPhase } from '../../shared/types'; +import { CompetitorAnalysisDialog } from './CompetitorAnalysisDialog'; +import type { RoadmapFeature, RoadmapPhase, CompetitorAnalysis, CompetitorPainPoint } from '../../shared/types'; interface RoadmapProps { projectId: string; @@ -50,10 +51,13 @@ interface RoadmapProps { export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { const roadmap = useRoadmapStore((state) => state.roadmap); + const competitorAnalysis = useRoadmapStore((state) => state.competitorAnalysis); const generationStatus = useRoadmapStore((state) => state.generationStatus); const updateFeatureLinkedSpec = useRoadmapStore((state) => state.updateFeatureLinkedSpec); const [selectedFeature, setSelectedFeature] = useState(null); const [activeTab, setActiveTab] = useState('phases'); + const [showCompetitorDialog, setShowCompetitorDialog] = useState(false); + const [pendingAction, setPendingAction] = useState<'generate' | 'refresh' | null>(null); // Load roadmap on mount useEffect(() => { @@ -61,11 +65,31 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { }, [projectId]); const handleGenerate = () => { - generateRoadmap(projectId); + setPendingAction('generate'); + setShowCompetitorDialog(true); }; const handleRefresh = () => { - refreshRoadmap(projectId); + setPendingAction('refresh'); + setShowCompetitorDialog(true); + }; + + const handleCompetitorDialogAccept = () => { + if (pendingAction === 'generate') { + generateRoadmap(projectId, true); + } else if (pendingAction === 'refresh') { + refreshRoadmap(projectId, true); + } + setPendingAction(null); + }; + + const handleCompetitorDialogDecline = () => { + if (pendingAction === 'generate') { + generateRoadmap(projectId, false); + } else if (pendingAction === 'refresh') { + refreshRoadmap(projectId, false); + } + setPendingAction(null); }; const handleConvertToSpec = async (feature: RoadmapFeature) => { @@ -92,6 +116,23 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { const stats = getFeatureStats(roadmap); + // Helper function to get competitor insights for a feature + const getCompetitorInsightsForFeature = (feature: RoadmapFeature): CompetitorPainPoint[] => { + if (!competitorAnalysis || !feature.competitorInsightIds || feature.competitorInsightIds.length === 0) { + return []; + } + + const insights: CompetitorPainPoint[] = []; + for (const competitor of competitorAnalysis.competitors) { + for (const painPoint of competitor.painPoints) { + if (feature.competitorInsightIds.includes(painPoint.id)) { + insights.push(painPoint); + } + } + } + return insights; + }; + // Show generation progress if (generationStatus.phase !== 'idle' && generationStatus.phase !== 'complete') { return ( @@ -116,20 +157,28 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { // Show empty state if (!roadmap) { return ( -
- - -

No Roadmap Yet

-

- Generate an AI-powered roadmap that understands your project's target - audience and creates a strategic feature plan. -

- -
-
+ <> +
+ + +

No Roadmap Yet

+

+ Generate an AI-powered roadmap that understands your project's target + audience and creates a strategic feature plan. +

+ +
+
+ + ); } @@ -237,7 +286,9 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { onClick={() => setSelectedFeature(feature)} onConvertToSpec={handleConvertToSpec} onGoToTask={handleGoToTask} - /> + hasCompetitorInsight={ + !!feature.competitorInsightIds && feature.competitorInsightIds.length > 0 + } /> ))} @@ -268,13 +319,19 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { onClick={() => setSelectedFeature(feature)} >
{feature.title}
-
+
{feature.complexity} {feature.impact} impact + {feature.competitorInsightIds && feature.competitorInsightIds.length > 0 && ( + + + Insight + + )}
))} @@ -294,8 +351,16 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { onClose={() => setSelectedFeature(null)} onConvertToSpec={handleConvertToSpec} onGoToTask={handleGoToTask} - /> + competitorInsights={getCompetitorInsightsForFeature(selectedFeature)} /> )} + + {/* Competitor Analysis Permission Dialog */} + ); } @@ -388,17 +453,20 @@ function PhaseCard({ phase, features, isFirst, onFeatureSelect, onConvertToSpec, className="flex items-center justify-between p-2 rounded-md bg-muted/50 hover:bg-muted cursor-pointer transition-colors" onClick={() => onFeatureSelect(feature)} > -
+
{feature.priority} - {feature.title} + {feature.title} + {feature.competitorInsightIds && feature.competitorInsightIds.length > 0 && ( + + )}
{feature.status === 'done' ? ( - + ) : feature.linkedSpecId ? ( - ) : ( + ) : (