feat: add accept all functionality in ideation view

- Introduced a new "Accept All" button in the IdeationHeader component, allowing users to accept all filtered suggestions at once.
- Implemented state management for accept all readiness and count in the IdeationView component.
- Enhanced the IdeationDashboard to notify the parent component about the readiness of the accept all feature.
- Added logic to handle the acceptance of all suggestions, including success and failure notifications.
- Updated UI components to reflect the new functionality and improve user experience.
This commit is contained in:
Shirone
2026-01-11 10:01:01 +01:00
parent 5f3db1f25e
commit 2d4ffc7514
2 changed files with 123 additions and 13 deletions

View File

@@ -3,7 +3,7 @@
* First page users see - shows all ideas ready for accept/reject
*/
import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect, useCallback } 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';
@@ -17,6 +17,7 @@ import type { AnalysisSuggestion } from '@automaker/types';
interface IdeationDashboardProps {
onGenerateIdeas: () => void;
onAcceptAllReady?: (isReady: boolean, count: number, handler: () => Promise<void>) => void;
}
function SuggestionCard({
@@ -37,14 +38,16 @@ function SuggestionCard({
<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 className="flex items-start gap-2 mb-1">
<h4 className="font-medium shrink-0">{suggestion.title}</h4>
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className="text-xs whitespace-nowrap">
{suggestion.priority}
</Badge>
<Badge variant="secondary" className="text-xs whitespace-nowrap">
{job.prompt.title}
</Badge>
</div>
</div>
<p className="text-sm text-muted-foreground">{suggestion.description}</p>
{suggestion.rationale && (
@@ -166,11 +169,12 @@ function TagFilter({
);
}
export function IdeationDashboard({ onGenerateIdeas }: IdeationDashboardProps) {
export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: IdeationDashboardProps) {
const currentProject = useAppStore((s) => s.currentProject);
const generationJobs = useIdeationStore((s) => s.generationJobs);
const removeSuggestionFromJob = useIdeationStore((s) => s.removeSuggestionFromJob);
const [addingId, setAddingId] = useState<string | null>(null);
const [isAcceptingAll, setIsAcceptingAll] = useState(false);
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
// Get jobs for current project only (memoized to prevent unnecessary re-renders)
@@ -270,6 +274,54 @@ export function IdeationDashboard({ onGenerateIdeas }: IdeationDashboardProps) {
toast.info('Idea removed');
};
// Accept all filtered suggestions
const handleAcceptAll = useCallback(async () => {
if (!currentProject?.path || filteredSuggestions.length === 0) {
return;
}
setIsAcceptingAll(true);
const api = getElectronAPI();
let successCount = 0;
let failCount = 0;
// Process all filtered suggestions
for (const { suggestion, job } of filteredSuggestions) {
try {
const result = await api.ideation?.addSuggestionToBoard(currentProject.path, suggestion);
if (result?.success) {
removeSuggestionFromJob(job.id, suggestion.id);
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error('Failed to add suggestion to board:', error);
failCount++;
}
}
setIsAcceptingAll(false);
if (successCount > 0 && failCount === 0) {
toast.success(`Added ${successCount} idea${successCount > 1 ? 's' : ''} to board`);
} else if (successCount > 0 && failCount > 0) {
toast.warning(
`Added ${successCount} idea${successCount > 1 ? 's' : ''}, ${failCount} failed`
);
} else {
toast.error('Failed to add ideas to board');
}
}, [currentProject?.path, filteredSuggestions, removeSuggestionFromJob]);
// Notify parent about accept all readiness
useEffect(() => {
if (onAcceptAllReady) {
const isReady = filteredSuggestions.length > 0 && !isAcceptingAll && !addingId;
onAcceptAllReady(isReady, filteredSuggestions.length, handleAcceptAll);
}
}, [filteredSuggestions.length, isAcceptingAll, addingId, handleAcceptAll, onAcceptAllReady]);
const isEmpty = allSuggestions.length === 0 && activeJobs.length === 0;
return (

View File

@@ -3,7 +3,7 @@
* Dashboard-first design with Generate Ideas flow
*/
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { useIdeationStore } from '@/store/ideation-store';
import { useAppStore } from '@/store/app-store';
import { PromptCategoryGrid } from './components/prompt-category-grid';
@@ -11,7 +11,7 @@ import { PromptList } from './components/prompt-list';
import { IdeationDashboard } from './components/ideation-dashboard';
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
import { Button } from '@/components/ui/button';
import { ArrowLeft, ChevronRight, Lightbulb } from 'lucide-react';
import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Loader2 } from 'lucide-react';
import type { IdeaCategory } from '@automaker/types';
import type { IdeationMode } from '@/store/ideation-store';
@@ -67,12 +67,20 @@ function IdeationHeader({
onNavigate,
onGenerateIdeas,
onBack,
acceptAllReady,
acceptAllCount,
onAcceptAll,
isAcceptingAll,
}: {
currentMode: IdeationMode;
selectedCategory: IdeaCategory | null;
onNavigate: (mode: IdeationMode, category?: IdeaCategory | null) => void;
onGenerateIdeas: () => void;
onBack: () => void;
acceptAllReady: boolean;
acceptAllCount: number;
onAcceptAll: () => void;
isAcceptingAll: boolean;
}) {
const { getCategoryById } = useGuidedPrompts();
const showBackButton = currentMode === 'prompts';
@@ -120,6 +128,21 @@ function IdeationHeader({
</div>
<div className="flex gap-2 items-center">
{currentMode === 'dashboard' && acceptAllReady && (
<Button
onClick={onAcceptAll}
variant="outline"
className="gap-2"
disabled={isAcceptingAll}
>
{isAcceptingAll ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<CheckCheck className="w-4 h-4" />
)}
Accept All ({acceptAllCount})
</Button>
)}
<Button onClick={onGenerateIdeas} className="gap-2">
<Lightbulb className="w-4 h-4" />
Generate Ideas
@@ -133,6 +156,32 @@ export function IdeationView() {
const currentProject = useAppStore((s) => s.currentProject);
const { currentMode, selectedCategory, setMode, setCategory } = useIdeationStore();
// Accept all state
const [acceptAllReady, setAcceptAllReady] = useState(false);
const [acceptAllCount, setAcceptAllCount] = useState(0);
const [acceptAllHandler, setAcceptAllHandler] = useState<(() => Promise<void>) | null>(null);
const [isAcceptingAll, setIsAcceptingAll] = useState(false);
const handleAcceptAllReady = useCallback(
(isReady: boolean, count: number, handler: () => Promise<void>) => {
setAcceptAllReady(isReady);
setAcceptAllCount(count);
setAcceptAllHandler(() => handler);
},
[]
);
const handleAcceptAll = useCallback(async () => {
if (acceptAllHandler) {
setIsAcceptingAll(true);
try {
await acceptAllHandler();
} finally {
setIsAcceptingAll(false);
}
}
}, [acceptAllHandler]);
const handleNavigate = useCallback(
(mode: IdeationMode, category?: IdeaCategory | null) => {
setMode(mode);
@@ -192,10 +241,19 @@ export function IdeationView() {
onNavigate={handleNavigate}
onGenerateIdeas={handleGenerateIdeas}
onBack={handleBackFromPrompts}
acceptAllReady={acceptAllReady}
acceptAllCount={acceptAllCount}
onAcceptAll={handleAcceptAll}
isAcceptingAll={isAcceptingAll}
/>
{/* Dashboard - main view */}
{currentMode === 'dashboard' && <IdeationDashboard onGenerateIdeas={handleGenerateIdeas} />}
{currentMode === 'dashboard' && (
<IdeationDashboard
onGenerateIdeas={handleGenerateIdeas}
onAcceptAllReady={handleAcceptAllReady}
/>
)}
{/* Prompts - category selection */}
{currentMode === 'prompts' && !selectedCategory && (