"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, } from "lucide-react"; import { getElectronAPI, FeatureSuggestion, SuggestionsEvent } 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; } 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 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))); toast.success(`Generated ${event.suggestions.length} feature suggestions!`); } 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]); // Start generating suggestions const handleGenerate = useCallback(async () => { const api = getElectronAPI(); if (!api?.suggestions) { toast.error("Suggestions API not available"); return; } setIsGenerating(true); setProgress([]); setSuggestions([]); setSelectedIds(new Set()); try { const result = await api.suggestions.generate(projectPath); 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 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([]); 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; }; // Reset state when dialog closes useEffect(() => { if (!open) { // Don't reset immediately - allow re-open to see results // Only reset if explicitly closed without importing } }, [open]); const hasStarted = progress.length > 0 || suggestions.length > 0; const hasSuggestions = suggestions.length > 0; return ( Feature Suggestions Analyze your project to discover missing features and improvements. The AI will scan your codebase and suggest features ordered by priority. {!hasStarted ? ( // Initial state - show explanation and generate button

Discover Missing Features

Our AI will analyze your project structure, code patterns, and existing features to generate a prioritized list of suggestions for new features you could add.

) : 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.

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