feat: add discard all functionality to ideation view

- Introduced a new button in the IdeationHeader for discarding all ideas when in dashboard mode.
- Implemented state management for discard readiness and count in IdeationView.
- Added confirmation dialog for discarding ideas in IdeationDashboard.
- Enhanced bulk action readiness checks to include discard operations.

This update improves user experience by allowing bulk discarding of ideas with confirmation, ensuring actions are intentional.
This commit is contained in:
Shirone
2026-01-15 22:37:26 +01:00
parent 05a3b95d75
commit 07d800b589
2 changed files with 96 additions and 15 deletions

View File

@@ -4,10 +4,11 @@
*/ */
import { useState, useMemo, useEffect, useCallback } from 'react'; import { useState, useMemo, useEffect, useCallback } from 'react';
import { Loader2, AlertCircle, Plus, X, Sparkles, Lightbulb } from 'lucide-react'; import { Loader2, AlertCircle, Plus, X, Sparkles, Lightbulb, Trash2 } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { useIdeationStore, type GenerationJob } from '@/store/ideation-store'; import { useIdeationStore, type GenerationJob } from '@/store/ideation-store';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
@@ -15,9 +16,13 @@ import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { AnalysisSuggestion } from '@automaker/types'; import type { AnalysisSuggestion } from '@automaker/types';
// Helper for consistent pluralization of "idea/ideas"
const pluralizeIdea = (count: number) => `idea${count !== 1 ? 's' : ''}`;
interface IdeationDashboardProps { interface IdeationDashboardProps {
onGenerateIdeas: () => void; onGenerateIdeas: () => void;
onAcceptAllReady?: (isReady: boolean, count: number, handler: () => Promise<void>) => void; onAcceptAllReady?: (isReady: boolean, count: number, handler: () => Promise<void>) => void;
onDiscardAllReady?: (isReady: boolean, count: number, handler: () => void) => void;
} }
function SuggestionCard({ function SuggestionCard({
@@ -169,13 +174,18 @@ function TagFilter({
); );
} }
export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: IdeationDashboardProps) { export function IdeationDashboard({
onGenerateIdeas,
onAcceptAllReady,
onDiscardAllReady,
}: IdeationDashboardProps) {
const currentProject = useAppStore((s) => s.currentProject); const currentProject = useAppStore((s) => s.currentProject);
const generationJobs = useIdeationStore((s) => s.generationJobs); const generationJobs = useIdeationStore((s) => s.generationJobs);
const removeSuggestionFromJob = useIdeationStore((s) => s.removeSuggestionFromJob); const removeSuggestionFromJob = useIdeationStore((s) => s.removeSuggestionFromJob);
const [addingId, setAddingId] = useState<string | null>(null); const [addingId, setAddingId] = useState<string | null>(null);
const [isAcceptingAll, setIsAcceptingAll] = useState(false); const [isAcceptingAll, setIsAcceptingAll] = useState(false);
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set()); const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
// Get jobs for current project only (memoized to prevent unnecessary re-renders) // Get jobs for current project only (memoized to prevent unnecessary re-renders)
const projectJobs = useMemo( const projectJobs = useMemo(
@@ -304,23 +314,40 @@ export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: Ideatio
setIsAcceptingAll(false); setIsAcceptingAll(false);
if (successCount > 0 && failCount === 0) { if (successCount > 0 && failCount === 0) {
toast.success(`Added ${successCount} idea${successCount > 1 ? 's' : ''} to board`); toast.success(`Added ${successCount} ${pluralizeIdea(successCount)} to board`);
} else if (successCount > 0 && failCount > 0) { } else if (successCount > 0 && failCount > 0) {
toast.warning( toast.warning(`Added ${successCount} ${pluralizeIdea(successCount)}, ${failCount} failed`);
`Added ${successCount} idea${successCount > 1 ? 's' : ''}, ${failCount} failed`
);
} else { } else {
toast.error('Failed to add ideas to board'); toast.error('Failed to add ideas to board');
} }
}, [currentProject?.path, filteredSuggestions, removeSuggestionFromJob]); }, [currentProject?.path, filteredSuggestions, removeSuggestionFromJob]);
// Show discard confirmation dialog
const handleDiscardAll = useCallback(() => {
setShowDiscardConfirm(true);
}, []);
// Actually discard all filtered suggestions
const confirmDiscardAll = useCallback(() => {
const count = filteredSuggestions.length;
for (const { suggestion, job } of filteredSuggestions) {
removeSuggestionFromJob(job.id, suggestion.id);
}
toast.info(`Discarded ${count} ${pluralizeIdea(count)}`);
}, [filteredSuggestions, removeSuggestionFromJob]);
// Common readiness state for bulk operations
const bulkActionsReady = filteredSuggestions.length > 0 && !isAcceptingAll && !addingId;
// Notify parent about accept all readiness // Notify parent about accept all readiness
useEffect(() => { useEffect(() => {
if (onAcceptAllReady) { onAcceptAllReady?.(bulkActionsReady, filteredSuggestions.length, handleAcceptAll);
const isReady = filteredSuggestions.length > 0 && !isAcceptingAll && !addingId; }, [bulkActionsReady, filteredSuggestions.length, handleAcceptAll, onAcceptAllReady]);
onAcceptAllReady(isReady, filteredSuggestions.length, handleAcceptAll);
} // Notify parent about discard all readiness
}, [filteredSuggestions.length, isAcceptingAll, addingId, handleAcceptAll, onAcceptAllReady]); useEffect(() => {
onDiscardAllReady?.(bulkActionsReady, filteredSuggestions.length, handleDiscardAll);
}, [bulkActionsReady, filteredSuggestions.length, handleDiscardAll, onDiscardAllReady]);
const isEmpty = allSuggestions.length === 0 && activeJobs.length === 0; const isEmpty = allSuggestions.length === 0 && activeJobs.length === 0;
@@ -331,10 +358,10 @@ export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: Ideatio
{(generatingCount > 0 || allSuggestions.length > 0) && ( {(generatingCount > 0 || allSuggestions.length > 0) && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{generatingCount > 0 {generatingCount > 0
? `Generating ${generatingCount} idea${generatingCount > 1 ? 's' : ''}...` ? `Generating ${generatingCount} ${pluralizeIdea(generatingCount)}...`
: selectedTags.size > 0 : selectedTags.size > 0
? `Showing ${filteredSuggestions.length} of ${allSuggestions.length} ideas` ? `Showing ${filteredSuggestions.length} of ${allSuggestions.length} ${pluralizeIdea(allSuggestions.length)}`
: `${allSuggestions.length} idea${allSuggestions.length > 1 ? 's' : ''} ready for review`} : `${allSuggestions.length} ${pluralizeIdea(allSuggestions.length)} ready for review`}
</p> </p>
)} )}
@@ -419,6 +446,19 @@ export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: Ideatio
</Card> </Card>
)} )}
</div> </div>
{/* Discard All Confirmation Dialog */}
<ConfirmDialog
open={showDiscardConfirm}
onOpenChange={setShowDiscardConfirm}
onConfirm={confirmDiscardAll}
title="Discard All Ideas"
description={`Are you sure you want to discard ${filteredSuggestions.length} ${pluralizeIdea(filteredSuggestions.length)}? This cannot be undone.`}
icon={Trash2}
iconClassName="text-destructive"
confirmText="Discard"
confirmVariant="destructive"
/>
</div> </div>
); );
} }

View File

@@ -11,7 +11,7 @@ import { PromptList } from './components/prompt-list';
import { IdeationDashboard } from './components/ideation-dashboard'; import { IdeationDashboard } from './components/ideation-dashboard';
import { useGuidedPrompts } from '@/hooks/use-guided-prompts'; import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Loader2 } from 'lucide-react'; import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Loader2, Trash2 } from 'lucide-react';
import type { IdeaCategory } from '@automaker/types'; import type { IdeaCategory } from '@automaker/types';
import type { IdeationMode } from '@/store/ideation-store'; import type { IdeationMode } from '@/store/ideation-store';
@@ -71,6 +71,9 @@ function IdeationHeader({
acceptAllCount, acceptAllCount,
onAcceptAll, onAcceptAll,
isAcceptingAll, isAcceptingAll,
discardAllReady,
discardAllCount,
onDiscardAll,
}: { }: {
currentMode: IdeationMode; currentMode: IdeationMode;
selectedCategory: IdeaCategory | null; selectedCategory: IdeaCategory | null;
@@ -81,6 +84,9 @@ function IdeationHeader({
acceptAllCount: number; acceptAllCount: number;
onAcceptAll: () => void; onAcceptAll: () => void;
isAcceptingAll: boolean; isAcceptingAll: boolean;
discardAllReady: boolean;
discardAllCount: number;
onDiscardAll: () => void;
}) { }) {
const { getCategoryById } = useGuidedPrompts(); const { getCategoryById } = useGuidedPrompts();
const showBackButton = currentMode === 'prompts'; const showBackButton = currentMode === 'prompts';
@@ -128,6 +134,17 @@ function IdeationHeader({
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{currentMode === 'dashboard' && discardAllReady && (
<Button
onClick={onDiscardAll}
variant="outline"
className="gap-2 text-destructive hover:text-destructive"
disabled={isAcceptingAll}
>
<Trash2 className="w-4 h-4" />
Discard All ({discardAllCount})
</Button>
)}
{currentMode === 'dashboard' && acceptAllReady && ( {currentMode === 'dashboard' && acceptAllReady && (
<Button <Button
onClick={onAcceptAll} onClick={onAcceptAll}
@@ -162,6 +179,11 @@ export function IdeationView() {
const [acceptAllHandler, setAcceptAllHandler] = useState<(() => Promise<void>) | null>(null); const [acceptAllHandler, setAcceptAllHandler] = useState<(() => Promise<void>) | null>(null);
const [isAcceptingAll, setIsAcceptingAll] = useState(false); const [isAcceptingAll, setIsAcceptingAll] = useState(false);
// Discard all state
const [discardAllReady, setDiscardAllReady] = useState(false);
const [discardAllCount, setDiscardAllCount] = useState(0);
const [discardAllHandler, setDiscardAllHandler] = useState<(() => void) | null>(null);
const handleAcceptAllReady = useCallback( const handleAcceptAllReady = useCallback(
(isReady: boolean, count: number, handler: () => Promise<void>) => { (isReady: boolean, count: number, handler: () => Promise<void>) => {
setAcceptAllReady(isReady); setAcceptAllReady(isReady);
@@ -182,6 +204,21 @@ export function IdeationView() {
} }
}, [acceptAllHandler]); }, [acceptAllHandler]);
const handleDiscardAllReady = useCallback(
(isReady: boolean, count: number, handler: () => void) => {
setDiscardAllReady(isReady);
setDiscardAllCount(count);
setDiscardAllHandler(() => handler);
},
[]
);
const handleDiscardAll = useCallback(() => {
if (discardAllHandler) {
discardAllHandler();
}
}, [discardAllHandler]);
const handleNavigate = useCallback( const handleNavigate = useCallback(
(mode: IdeationMode, category?: IdeaCategory | null) => { (mode: IdeationMode, category?: IdeaCategory | null) => {
setMode(mode); setMode(mode);
@@ -245,6 +282,9 @@ export function IdeationView() {
acceptAllCount={acceptAllCount} acceptAllCount={acceptAllCount}
onAcceptAll={handleAcceptAll} onAcceptAll={handleAcceptAll}
isAcceptingAll={isAcceptingAll} isAcceptingAll={isAcceptingAll}
discardAllReady={discardAllReady}
discardAllCount={discardAllCount}
onDiscardAll={handleDiscardAll}
/> />
{/* Dashboard - main view */} {/* Dashboard - main view */}
@@ -252,6 +292,7 @@ export function IdeationView() {
<IdeationDashboard <IdeationDashboard
onGenerateIdeas={handleGenerateIdeas} onGenerateIdeas={handleGenerateIdeas}
onAcceptAllReady={handleAcceptAllReady} onAcceptAllReady={handleAcceptAllReady}
onDiscardAllReady={handleDiscardAllReady}
/> />
)} )}