"use client"; 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, } from "lucide-react"; import { getElectronAPI, FeatureSuggestion, SuggestionsEvent, SuggestionType } from "@/lib/electron"; import { useAppStore, Feature } from "@/store/app-store"; import { toast } from "sonner"; 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; 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([]); const [selectedIds, setSelectedIds] = useState>(new Set()); const [expandedIds, setExpandedIds] = useState>(new Set()); const [isImporting, setIsImporting] = useState(false); const [currentSuggestionType, setCurrentSuggestionType] = useState(null); const scrollRef = useRef(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"; setProgress((prev) => [...prev, `Using tool: ${toolName}\n`]); } 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: s.steps, status: "backlog" as const, skipTests: true, // As specified, testing mode true })); // 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 = progress.length > 0 || suggestions.length > 0; const hasSuggestions = suggestions.length > 0; const currentConfig = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType] : null; return ( {currentConfig ? ( <> {currentConfig.label} ) : ( <> AI Suggestions )} {currentConfig ? currentConfig.description : "Analyze your project to discover improvements. Choose a suggestion type below."} {!hasStarted ? ( // Initial state - show suggestion type buttons

Our AI will analyze your project and generate actionable suggestions. Choose what type of analysis you want to perform:

{(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map( ([type, config]) => { const Icon = config.icon; return ( ); } )}
) : isGenerating ? ( // Generating state - show progress
Analyzing project...
{progress.join("")}
) : hasSuggestions ? ( // Results state - show suggestions list
{suggestions.length} suggestions generated
{selectedIds.size} selected
{suggestions.map((suggestion) => { const isSelected = selectedIds.has(suggestion.id); const isExpanded = expandedIds.has(suggestion.id); return (
toggleSelection(suggestion.id)} className="mt-1" />
#{suggestion.priority} {suggestion.category}
{isExpanded && (
{suggestion.reasoning && (

{suggestion.reasoning}

)} {suggestion.steps.length > 0 && (

Implementation Steps:

    {suggestion.steps.map((step, i) => (
  • {step}
  • ))}
)}
)}
); })}
) : ( // No results state

No suggestions were generated. Try running the analysis again.

{currentSuggestionType && ( )}
)} {hasSuggestions && (
{currentSuggestionType && ( )}
{isImporting ? ( ) : ( )} Import {selectedIds.size} Feature {selectedIds.size !== 1 ? "s" : ""}
)} {!hasSuggestions && !isGenerating && hasStarted && ( )}
); }