fix: address multiple CodeRabbit review feedback items

Changes included:

1. **InvestigationDialog.tsx** - Prevent state updates after unmount:
   - Add isMounted flag to prevent state updates after component unmounts
   - Add fetchCommentsError state to surface API errors to the user
   - Display error state in UI with styled error message
   - Ensure cleanup function properly sets isMounted = false

2. **EnvConfigModal.tsx** - Improve type safety:
   - Replace `any` type with proper `ClaudeProfile` type in filter callback

3. **GenerationProgressScreen.tsx** & **RoadmapGenerationProgress.tsx**:
   - Add double-click prevention for stop button
   - Add isStopping state with proper error handling
This commit is contained in:
AndyMik90
2025-12-18 20:40:58 +01:00
parent 892e01d608
commit 48f7c3cc61
5 changed files with 67 additions and 10 deletions
@@ -30,6 +30,7 @@ import {
TooltipTrigger
} from './ui/tooltip';
import { cn } from '../lib/utils';
import type { ClaudeProfile } from '../../shared/types';
interface EnvConfigModalProps {
open: boolean;
@@ -101,7 +102,7 @@ export function EnvConfigModal({
// Handle Claude profiles
if (profilesResult.success && profilesResult.data) {
const authenticatedProfiles = profilesResult.data.profiles.filter(
(p: any) => p.oauthToken || (p.isDefault && p.configDir)
(p: ClaudeProfile) => p.oauthToken || (p.isDefault && p.configDir)
);
setClaudeProfiles(authenticatedProfiles);
@@ -197,6 +197,23 @@ export function RoadmapGenerationProgress({
}: RoadmapGenerationProgressProps) {
const { phase, progress, message, error } = generationStatus;
const reducedMotion = useReducedMotion();
const [isStopping, setIsStopping] = useState(false);
/**
* Handle stop button click with error handling and double-click prevention
*/
const handleStopClick = async () => {
if (!onStop || isStopping) return;
setIsStopping(true);
try {
await onStop();
} catch (err) {
console.error('Failed to stop generation:', err);
} finally {
setIsStopping(false);
}
};
// Don't render anything for idle phase
if (phase === 'idle') {
@@ -260,10 +277,11 @@ export function RoadmapGenerationProgress({
<Button
variant="destructive"
size="sm"
onClick={onStop}
onClick={handleStopClick}
disabled={isStopping}
>
<Square className="h-4 w-4 mr-1" />
Stop
{isStopping ? 'Stopping...' : 'Stop'}
</Button>
</TooltipTrigger>
<TooltipContent>Stop generation</TooltipContent>
@@ -35,16 +35,21 @@ export function InvestigationDialog({
const [comments, setComments] = useState<GitHubComment[]>([]);
const [selectedCommentIds, setSelectedCommentIds] = useState<number[]>([]);
const [loadingComments, setLoadingComments] = useState(false);
const [fetchCommentsError, setFetchCommentsError] = useState<string | null>(null);
// Fetch comments when dialog opens
useEffect(() => {
if (open && selectedIssue && projectId) {
let isMounted = true;
setLoadingComments(true);
setComments([]);
setSelectedCommentIds([]);
setFetchCommentsError(null);
window.electronAPI.getIssueComments(projectId, selectedIssue.number)
.then((result: { success: boolean; data?: GitHubComment[] }) => {
if (!isMounted) return;
if (result.success && result.data) {
setComments(result.data);
// By default, select all comments
@@ -52,11 +57,21 @@ export function InvestigationDialog({
}
})
.catch((err: unknown) => {
if (!isMounted) return;
console.error('Failed to fetch comments:', err);
setFetchCommentsError(
err instanceof Error ? err.message : 'Failed to load comments'
);
})
.finally(() => {
setLoadingComments(false);
if (isMounted) {
setLoadingComments(false);
}
});
return () => {
isMounted = false;
};
}
}, [open, selectedIssue, projectId]);
@@ -108,6 +123,11 @@ export function InvestigationDialog({
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : fetchCommentsError ? (
<div className="rounded-lg bg-destructive/10 border border-destructive/30 p-4">
<p className="text-sm text-destructive font-medium">Failed to load comments</p>
<p className="text-xs text-destructive/80 mt-1">{fetchCommentsError}</p>
</div>
) : comments.length > 0 ? (
<div className="space-y-2 flex-1 min-h-0 flex flex-col">
<div className="flex items-center justify-between">
@@ -51,6 +51,23 @@ export function GenerationProgressScreen({
}: GenerationProgressScreenProps) {
const logsEndRef = useRef<HTMLDivElement>(null);
const [showLogs, setShowLogs] = useState(false);
const [isStopping, setIsStopping] = useState(false);
/**
* Handle stop button click with error handling and double-click prevention
*/
const handleStopClick = async () => {
if (isStopping) return;
setIsStopping(true);
try {
await onStop();
} catch (err) {
console.error('Failed to stop generation:', err);
} finally {
setIsStopping(false);
}
};
// Auto-scroll to bottom when logs update
useEffect(() => {
@@ -98,10 +115,11 @@ export function GenerationProgressScreen({
<Button
variant="destructive"
size="sm"
onClick={onStop}
onClick={handleStopClick}
disabled={isStopping}
>
<Square className="h-4 w-4 mr-1" />
Stop
{isStopping ? 'Stopping...' : 'Stop'}
</Button>
</TooltipTrigger>
<TooltipContent>Stop generation</TooltipContent>
@@ -28,8 +28,8 @@ export function RoadmapHeader({ roadmap, competitorAnalysis, onAddFeature, onRef
<TooltipContent className="max-w-md">
<div className="space-y-2">
<div className="font-semibold">Analyzed {competitorAnalysis.competitors.length} competitors:</div>
{competitorAnalysis.competitors.map((comp: { name: string; painPoints: unknown[] }, idx: number) => (
<div key={idx} className="text-sm">
{competitorAnalysis.competitors.map((comp) => (
<div key={comp.id} className="text-sm">
<div className="font-medium"> {comp.name}</div>
<div className="text-muted-foreground ml-3">{comp.painPoints.length} pain points identified</div>
</div>
@@ -79,8 +79,8 @@ export function RoadmapHeader({ roadmap, competitorAnalysis, onAddFeature, onRef
<TooltipContent className="max-w-md">
<div className="space-y-1">
<div className="font-semibold mb-2">Secondary Personas:</div>
{roadmap.targetAudience.secondary.map((persona, idx) => (
<div key={idx} className="text-sm"> {persona}</div>
{roadmap.targetAudience.secondary.map((persona) => (
<div key={persona} className="text-sm"> {persona}</div>
))}
</div>
</TooltipContent>