refactor: extract Enhance with AI into shared components

Extract all "Enhance with AI" functionality into reusable shared components
following DRY principles and clean code guidelines.

Changes:
- Create shared/enhancement/ folder for related functionality
- Extract EnhanceWithAI component (AI enhancement with model override)
- Extract EnhancementHistoryButton component (version history UI)
- Extract enhancement constants and types
- Refactor add-feature-dialog.tsx to use shared components
- Refactor edit-feature-dialog.tsx to use shared components
- Refactor follow-up-dialog.tsx to use shared components
- Add history tracking to add-feature-dialog for consistency

Benefits:
- Eliminated ~527 lines of duplicated code
- Single source of truth for enhancement logic
- Consistent UX across all dialogs
- Easier maintenance and extensibility
- Better code organization

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-11 15:10:54 +01:00
parent 6e4b611662
commit 8321c06e16
11 changed files with 441 additions and 337 deletions

View File

@@ -161,12 +161,14 @@ export function BoardView() {
followUpPrompt, followUpPrompt,
followUpImagePaths, followUpImagePaths,
followUpPreviewMap, followUpPreviewMap,
followUpPromptHistory,
setShowFollowUpDialog, setShowFollowUpDialog,
setFollowUpFeature, setFollowUpFeature,
setFollowUpPrompt, setFollowUpPrompt,
setFollowUpImagePaths, setFollowUpImagePaths,
setFollowUpPreviewMap, setFollowUpPreviewMap,
handleFollowUpDialogChange, handleFollowUpDialogChange,
addToPromptHistory,
} = useFollowUpState(); } = useFollowUpState();
// Selection mode hook for mass editing // Selection mode hook for mass editing
@@ -1422,6 +1424,8 @@ export function BoardView() {
onPreviewMapChange={setFollowUpPreviewMap} onPreviewMapChange={setFollowUpPreviewMap}
onSend={handleSendFollowUp} onSend={handleSendFollowUp}
isMaximized={isMaximized} isMaximized={isMaximized}
promptHistory={followUpPromptHistory}
onHistoryAdd={addToPromptHistory}
/> />
{/* Backlog Plan Dialog */} {/* Backlog Plan Dialog */}

View File

@@ -21,11 +21,9 @@ import {
FeatureTextFilePath as DescriptionTextFilePath, FeatureTextFilePath as DescriptionTextFilePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Play, Cpu, FolderKanban } from 'lucide-react';
import { Sparkles, ChevronDown, ChevronRight, Play, Cpu, FolderKanban } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
import { modelSupportsThinking } from '@/lib/utils'; import { modelSupportsThinking } from '@/lib/utils';
import { import {
useAppStore, useAppStore,
@@ -43,16 +41,12 @@ import {
WorkModeSelector, WorkModeSelector,
PlanningModeSelect, PlanningModeSelect,
AncestorContextSection, AncestorContextSection,
EnhanceWithAI,
EnhancementHistoryButton,
type BaseHistoryEntry,
} from '../shared'; } from '../shared';
import type { WorkMode } from '../shared'; import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { import {
getAncestors, getAncestors,
@@ -139,11 +133,12 @@ export function AddFeatureDialog({
// UI state // UI state
const [previewMap, setPreviewMap] = useState<ImagePreviewMap>(() => new Map()); const [previewMap, setPreviewMap] = useState<ImagePreviewMap>(() => new Map());
const [descriptionError, setDescriptionError] = useState(false); const [descriptionError, setDescriptionError] = useState(false);
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState< // Description history state
'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' interface DescriptionHistoryEntry extends BaseHistoryEntry {
>('improve'); description: string;
const [enhanceOpen, setEnhanceOpen] = useState(false); }
const [descriptionHistory, setDescriptionHistory] = useState<DescriptionHistoryEntry[]>([]);
// Spawn mode state // Spawn mode state
const [ancestors, setAncestors] = useState<AncestorContext[]>([]); const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
@@ -152,9 +147,6 @@ export function AddFeatureDialog({
// Get defaults from store // Get defaults from store
const { defaultPlanningMode, defaultRequirePlanApproval } = useAppStore(); const { defaultPlanningMode, defaultRequirePlanApproval } = useAppStore();
// Enhancement model override
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
// Track previous open state to detect when dialog opens // Track previous open state to detect when dialog opens
const wasOpenRef = useRef(false); const wasOpenRef = useRef(false);
@@ -171,6 +163,9 @@ export function AddFeatureDialog({
setRequirePlanApproval(defaultRequirePlanApproval); setRequirePlanApproval(defaultRequirePlanApproval);
setModelEntry({ model: 'opus' }); setModelEntry({ model: 'opus' });
// Initialize description history (empty for new feature)
setDescriptionHistory([]);
// Initialize ancestors for spawn mode // Initialize ancestors for spawn mode
if (parentFeature) { if (parentFeature) {
const ancestorList = getAncestors(parentFeature, allFeatures); const ancestorList = getAncestors(parentFeature, allFeatures);
@@ -279,7 +274,7 @@ export function AddFeatureDialog({
setRequirePlanApproval(defaultRequirePlanApproval); setRequirePlanApproval(defaultRequirePlanApproval);
setPreviewMap(new Map()); setPreviewMap(new Map());
setDescriptionError(false); setDescriptionError(false);
setEnhanceOpen(false); setDescriptionHistory([]);
onOpenChange(false); onOpenChange(false);
}; };
@@ -302,33 +297,6 @@ export function AddFeatureDialog({
} }
}; };
const handleEnhanceDescription = async () => {
if (!description.trim() || isEnhancing) return;
setIsEnhancing(true);
try {
const api = getElectronAPI();
const result = await api.enhancePrompt?.enhance(
description,
enhancementMode,
enhancementOverride.effectiveModel,
enhancementOverride.effectiveModelEntry.thinkingLevel
);
if (result?.success && result.enhancedText) {
setDescription(result.enhancedText);
toast.success('Description enhanced!');
} else {
toast.error(result?.error || 'Failed to enhance description');
}
} catch (error) {
logger.error('Enhancement failed:', error);
toast.error('Failed to enhance description');
} finally {
setIsEnhancing(false);
}
};
// Shared card styling // Shared card styling
const cardClass = 'rounded-lg border border-border/50 bg-muted/30 p-4 space-y-3'; const cardClass = 'rounded-lg border border-border/50 bg-muted/30 p-4 space-y-3';
const sectionHeaderClass = 'flex items-center gap-2 text-sm font-medium text-foreground'; const sectionHeaderClass = 'flex items-center gap-2 text-sm font-medium text-foreground';
@@ -380,7 +348,18 @@ export function AddFeatureDialog({
{/* Task Details Section */} {/* Task Details Section */}
<div className={cardClass}> <div className={cardClass}>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="description">Description</Label> <Label htmlFor="description">Description</Label>
{/* Version History Button */}
<EnhancementHistoryButton
history={descriptionHistory}
currentValue={description}
onRestore={setDescription}
valueAccessor={(entry) => entry.description}
title="Version History"
restoreMessage="Description restored from history"
/>
</div>
<DescriptionImageDropZone <DescriptionImageDropZone
value={description} value={description}
onChange={(value) => { onChange={(value) => {
@@ -409,76 +388,23 @@ export function AddFeatureDialog({
/> />
</div> </div>
{/* Collapsible Enhancement Section */} {/* Enhancement Section */}
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen}> <EnhanceWithAI
<CollapsibleTrigger asChild> value={description}
<button className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1"> onChange={setDescription}
{enhanceOpen ? ( onHistoryAdd={({ mode, enhancedText }) => {
<ChevronDown className="w-4 h-4" /> setDescriptionHistory((prev) => [
) : ( ...prev,
<ChevronRight className="w-4 h-4" /> {
)} description: enhancedText,
<Sparkles className="w-4 h-4" /> timestamp: new Date().toISOString(),
<span>Enhance with AI</span> source: 'enhance',
</button> enhancementMode: mode,
</CollapsibleTrigger> },
<CollapsibleContent className="pt-3"> ]);
<div className="flex flex-wrap items-center gap-2 pl-6"> }}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 text-xs">
{enhancementMode === 'improve' && 'Improve Clarity'}
{enhancementMode === 'technical' && 'Add Technical Details'}
{enhancementMode === 'simplify' && 'Simplify'}
{enhancementMode === 'acceptance' && 'Add Acceptance Criteria'}
{enhancementMode === 'ux-reviewer' && 'User Experience'}
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => setEnhancementMode('improve')}>
Improve Clarity
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('technical')}>
Add Technical Details
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('simplify')}>
Simplify
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('acceptance')}>
Add Acceptance Criteria
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('ux-reviewer')}>
User Experience
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant="default"
size="sm"
className="h-8 text-xs"
onClick={handleEnhanceDescription}
disabled={!description.trim() || isEnhancing}
loading={isEnhancing}
>
<Sparkles className="w-3 h-3 mr-1" />
Enhance
</Button>
<ModelOverrideTrigger
currentModelEntry={enhancementOverride.effectiveModelEntry}
onModelChange={enhancementOverride.setOverride}
phase="enhancementModel"
isOverridden={enhancementOverride.isOverridden}
size="sm"
variant="icon"
/> />
</div> </div>
</CollapsibleContent>
</Collapsible>
</div>
{/* AI & Execution Section */} {/* AI & Execution Section */}
<div className={cardClass}> <div className={cardClass}>

View File

@@ -21,18 +21,8 @@ import {
FeatureTextFilePath as DescriptionTextFilePath, FeatureTextFilePath as DescriptionTextFilePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { GitBranch, Cpu, FolderKanban } from 'lucide-react';
import {
Sparkles,
ChevronDown,
ChevronRight,
GitBranch,
History,
Cpu,
FolderKanban,
} from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
import { cn, modelSupportsThinking } from '@/lib/utils'; import { cn, modelSupportsThinking } from '@/lib/utils';
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store'; import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '@automaker/types'; import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '@automaker/types';
@@ -41,17 +31,11 @@ import {
PrioritySelector, PrioritySelector,
WorkModeSelector, WorkModeSelector,
PlanningModeSelect, PlanningModeSelect,
EnhanceWithAI,
EnhancementHistoryButton,
} from '../shared'; } from '../shared';
import type { WorkMode } from '../shared'; import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { DependencyTreeDialog } from './dependency-tree-dialog'; import { DependencyTreeDialog } from './dependency-tree-dialog';
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types'; import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
@@ -110,11 +94,6 @@ export function EditFeatureDialog({
const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>( const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>(
() => new Map() () => new Map()
); );
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState<
'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'
>('improve');
const [enhanceOpen, setEnhanceOpen] = useState(false);
const [showDependencyTree, setShowDependencyTree] = useState(false); const [showDependencyTree, setShowDependencyTree] = useState(false);
const [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip'); const [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip');
const [requirePlanApproval, setRequirePlanApproval] = useState( const [requirePlanApproval, setRequirePlanApproval] = useState(
@@ -137,11 +116,6 @@ export function EditFeatureDialog({
>(null); >(null);
// Track the original description when the dialog opened for comparison // Track the original description when the dialog opened for comparison
const [originalDescription, setOriginalDescription] = useState(feature?.description ?? ''); const [originalDescription, setOriginalDescription] = useState(feature?.description ?? '');
// Track if history dropdown is open
const [showHistory, setShowHistory] = useState(false);
// Enhancement model override
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
useEffect(() => { useEffect(() => {
setEditingFeature(feature); setEditingFeature(feature);
@@ -153,8 +127,6 @@ export function EditFeatureDialog({
// Reset history tracking state // Reset history tracking state
setOriginalDescription(feature.description ?? ''); setOriginalDescription(feature.description ?? '');
setDescriptionChangeSource(null); setDescriptionChangeSource(null);
setShowHistory(false);
setEnhanceOpen(false);
// Reset model entry // Reset model entry
setModelEntry({ setModelEntry({
model: (feature.model as ModelAlias) || 'opus', model: (feature.model as ModelAlias) || 'opus',
@@ -164,7 +136,6 @@ export function EditFeatureDialog({
} else { } else {
setEditFeaturePreviewMap(new Map()); setEditFeaturePreviewMap(new Map());
setDescriptionChangeSource(null); setDescriptionChangeSource(null);
setShowHistory(false);
} }
}, [feature]); }, [feature]);
@@ -237,36 +208,6 @@ export function EditFeatureDialog({
} }
}; };
const handleEnhanceDescription = async () => {
if (!editingFeature?.description.trim() || isEnhancing) return;
setIsEnhancing(true);
try {
const api = getElectronAPI();
const result = await api.enhancePrompt?.enhance(
editingFeature.description,
enhancementMode,
enhancementOverride.effectiveModel, // API accepts string, extract from PhaseModelEntry
enhancementOverride.effectiveModelEntry.thinkingLevel // Pass thinking level
);
if (result?.success && result.enhancedText) {
const enhancedText = result.enhancedText;
setEditingFeature((prev) => (prev ? { ...prev, description: enhancedText } : prev));
// Track that this change was from enhancement
setDescriptionChangeSource({ source: 'enhance', mode: enhancementMode });
toast.success('Description enhanced!');
} else {
toast.error(result?.error || 'Failed to enhance description');
}
} catch (error) {
logger.error('Enhancement failed:', error);
toast.error('Failed to enhance description');
} finally {
setIsEnhancing(false);
}
};
if (!editingFeature) { if (!editingFeature) {
return null; return null;
} }
@@ -306,92 +247,17 @@ export function EditFeatureDialog({
<Label htmlFor="edit-description">Description</Label> <Label htmlFor="edit-description">Description</Label>
{/* Version History Button */} {/* Version History Button */}
{feature?.descriptionHistory && feature.descriptionHistory.length > 0 && ( {feature?.descriptionHistory && feature.descriptionHistory.length > 0 && (
<Popover open={showHistory} onOpenChange={setShowHistory}> <EnhancementHistoryButton
<PopoverTrigger asChild> history={feature.descriptionHistory}
<Button currentValue={editingFeature.description}
type="button" onRestore={(description) => {
variant="ghost" setEditingFeature((prev) => (prev ? { ...prev, description } : prev));
size="sm"
className="h-7 gap-1.5 text-xs text-muted-foreground"
>
<History className="w-3.5 h-3.5" />
History ({feature.descriptionHistory.length})
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="p-3 border-b">
<h4 className="font-medium text-sm">Version History</h4>
<p className="text-xs text-muted-foreground mt-1">
Click a version to restore it
</p>
</div>
<div className="max-h-64 overflow-y-auto p-2 space-y-1">
{[...(feature.descriptionHistory || [])]
.reverse()
.map((entry: DescriptionHistoryEntry, index: number) => {
const isCurrentVersion =
entry.description === editingFeature.description;
const date = new Date(entry.timestamp);
const formattedDate = date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
const getEnhancementModeLabel = (mode?: string) => {
const labels: Record<string, string> = {
improve: 'Improve Clarity',
technical: 'Add Technical Details',
simplify: 'Simplify',
acceptance: 'Add Acceptance Criteria',
'ux-reviewer': 'User Experience',
};
return labels[mode || 'improve'] || mode || 'improve';
};
const sourceLabel =
entry.source === 'initial'
? 'Original'
: entry.source === 'enhance'
? `Enhanced (${getEnhancementModeLabel(entry.enhancementMode)})`
: 'Edited';
return (
<button
key={`${entry.timestamp}-${index}`}
onClick={() => {
setEditingFeature((prev) =>
prev ? { ...prev, description: entry.description } : prev
);
// Mark as edit since user is restoring from history
setDescriptionChangeSource('edit'); setDescriptionChangeSource('edit');
setShowHistory(false);
toast.success('Description restored from history');
}} }}
className={`w-full text-left p-2 rounded-md hover:bg-muted transition-colors ${ valueAccessor={(entry) => entry.description}
isCurrentVersion ? 'bg-muted/50 border border-primary/20' : '' title="Version History"
}`} restoreMessage="Description restored from history"
> />
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium">{sourceLabel}</span>
<span className="text-xs text-muted-foreground">
{formattedDate}
</span>
</div>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
{entry.description.slice(0, 100)}
{entry.description.length > 100 ? '...' : ''}
</p>
{isCurrentVersion && (
<span className="text-xs text-primary font-medium mt-1 block">
Current version
</span>
)}
</button>
);
})}
</div>
</PopoverContent>
</Popover>
)} )}
</div> </div>
<DescriptionImageDropZone <DescriptionImageDropZone
@@ -443,76 +309,15 @@ export function EditFeatureDialog({
/> />
</div> </div>
{/* Collapsible Enhancement Section */} {/* Enhancement Section */}
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen}> <EnhanceWithAI
<CollapsibleTrigger asChild> value={editingFeature.description}
<button className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1"> onChange={(enhanced) =>
{enhanceOpen ? ( setEditingFeature((prev) => (prev ? { ...prev, description: enhanced } : prev))
<ChevronDown className="w-4 h-4" /> }
) : ( onHistoryAdd={({ mode }) => setDescriptionChangeSource({ source: 'enhance', mode })}
<ChevronRight className="w-4 h-4" />
)}
<Sparkles className="w-4 h-4" />
<span>Enhance with AI</span>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="pt-3">
<div className="flex flex-wrap items-center gap-2 pl-6">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 text-xs">
{enhancementMode === 'improve' && 'Improve Clarity'}
{enhancementMode === 'technical' && 'Add Technical Details'}
{enhancementMode === 'simplify' && 'Simplify'}
{enhancementMode === 'acceptance' && 'Add Acceptance Criteria'}
{enhancementMode === 'ux-reviewer' && 'User Experience'}
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => setEnhancementMode('improve')}>
Improve Clarity
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('technical')}>
Add Technical Details
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('simplify')}>
Simplify
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('acceptance')}>
Add Acceptance Criteria
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('ux-reviewer')}>
User Experience
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant="default"
size="sm"
className="h-8 text-xs"
onClick={handleEnhanceDescription}
disabled={!editingFeature.description.trim() || isEnhancing}
loading={isEnhancing}
>
<Sparkles className="w-3 h-3 mr-1" />
Enhance
</Button>
<ModelOverrideTrigger
currentModelEntry={enhancementOverride.effectiveModelEntry}
onModelChange={enhancementOverride.setOverride}
phase="enhancementModel"
isOverridden={enhancementOverride.isOverridden}
size="sm"
variant="icon"
/> />
</div> </div>
</CollapsibleContent>
</Collapsible>
</div>
{/* AI & Execution Section */} {/* AI & Execution Section */}
<div className={cardClass}> <div className={cardClass}>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -17,6 +18,19 @@ import {
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { MessageSquare } from 'lucide-react'; import { MessageSquare } from 'lucide-react';
import { Feature } from '@/store/app-store'; import { Feature } from '@/store/app-store';
import { EnhanceWithAI, EnhancementHistoryButton, type EnhancementMode } from '../shared';
const logger = createLogger('FollowUpDialog');
/**
* A single entry in the follow-up prompt history
*/
export interface FollowUpHistoryEntry {
prompt: string;
timestamp: string; // ISO date string
source: 'initial' | 'enhance' | 'edit';
enhancementMode?: EnhancementMode;
}
interface FollowUpDialogProps { interface FollowUpDialogProps {
open: boolean; open: boolean;
@@ -30,6 +44,10 @@ interface FollowUpDialogProps {
onPreviewMapChange: (map: ImagePreviewMap) => void; onPreviewMapChange: (map: ImagePreviewMap) => void;
onSend: () => void; onSend: () => void;
isMaximized: boolean; isMaximized: boolean;
/** History of prompt versions for restoration */
promptHistory?: FollowUpHistoryEntry[];
/** Callback to add a new entry to prompt history */
onHistoryAdd?: (entry: FollowUpHistoryEntry) => void;
} }
export function FollowUpDialog({ export function FollowUpDialog({
@@ -44,9 +62,11 @@ export function FollowUpDialog({
onPreviewMapChange, onPreviewMapChange,
onSend, onSend,
isMaximized, isMaximized,
promptHistory = [],
onHistoryAdd,
}: FollowUpDialogProps) { }: FollowUpDialogProps) {
const handleClose = (open: boolean) => { const handleClose = (openState: boolean) => {
if (!open) { if (!openState) {
onOpenChange(false); onOpenChange(false);
} }
}; };
@@ -77,7 +97,18 @@ export function FollowUpDialog({
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0"> <div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="follow-up-prompt">Instructions</Label> <Label htmlFor="follow-up-prompt">Instructions</Label>
{/* Version History Button */}
<EnhancementHistoryButton
history={promptHistory}
currentValue={prompt}
onRestore={onPromptChange}
valueAccessor={(entry) => entry.prompt}
title="Prompt History"
restoreMessage="Prompt restored from history"
/>
</div>
<DescriptionImageDropZone <DescriptionImageDropZone
value={prompt} value={prompt}
onChange={onPromptChange} onChange={onPromptChange}
@@ -88,6 +119,21 @@ export function FollowUpDialog({
onPreviewMapChange={onPreviewMapChange} onPreviewMapChange={onPreviewMapChange}
/> />
</div> </div>
{/* Enhancement Section */}
<EnhanceWithAI
value={prompt}
onChange={onPromptChange}
onHistoryAdd={({ mode, enhancedText }) => {
onHistoryAdd?.({
prompt: enhancedText,
timestamp: new Date().toISOString(),
source: 'enhance',
enhancementMode: mode,
});
}}
/>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
The agent will continue from where it left off, using the existing context. You can The agent will continue from where it left off, using the existing context. You can
attach screenshots to help explain the issue. attach screenshots to help explain the issue.

View File

@@ -5,6 +5,6 @@ export { CompletedFeaturesModal } from './completed-features-modal';
export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog'; export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog';
export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog'; export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog';
export { EditFeatureDialog } from './edit-feature-dialog'; export { EditFeatureDialog } from './edit-feature-dialog';
export { FollowUpDialog } from './follow-up-dialog'; export { FollowUpDialog, type FollowUpHistoryEntry } from './follow-up-dialog';
export { PlanApprovalDialog } from './plan-approval-dialog'; export { PlanApprovalDialog } from './plan-approval-dialog';
export { MassEditDialog } from './mass-edit-dialog'; export { MassEditDialog } from './mass-edit-dialog';

View File

@@ -4,13 +4,18 @@ import {
FeatureImagePath as DescriptionImagePath, FeatureImagePath as DescriptionImagePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import type { FollowUpHistoryEntry } from '../dialogs/follow-up-dialog';
/**
* Custom hook for managing follow-up dialog state including prompt history
*/
export function useFollowUpState() { export function useFollowUpState() {
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false); const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null); const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
const [followUpPrompt, setFollowUpPrompt] = useState(''); const [followUpPrompt, setFollowUpPrompt] = useState('');
const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]); const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]);
const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(() => new Map()); const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(() => new Map());
const [followUpPromptHistory, setFollowUpPromptHistory] = useState<FollowUpHistoryEntry[]>([]);
const resetFollowUpState = useCallback(() => { const resetFollowUpState = useCallback(() => {
setShowFollowUpDialog(false); setShowFollowUpDialog(false);
@@ -18,6 +23,7 @@ export function useFollowUpState() {
setFollowUpPrompt(''); setFollowUpPrompt('');
setFollowUpImagePaths([]); setFollowUpImagePaths([]);
setFollowUpPreviewMap(new Map()); setFollowUpPreviewMap(new Map());
setFollowUpPromptHistory([]);
}, []); }, []);
const handleFollowUpDialogChange = useCallback( const handleFollowUpDialogChange = useCallback(
@@ -31,6 +37,13 @@ export function useFollowUpState() {
[resetFollowUpState] [resetFollowUpState]
); );
/**
* Adds a new entry to the prompt history
*/
const addToPromptHistory = useCallback((entry: FollowUpHistoryEntry) => {
setFollowUpPromptHistory((prev) => [...prev, entry]);
}, []);
return { return {
// State // State
showFollowUpDialog, showFollowUpDialog,
@@ -38,14 +51,17 @@ export function useFollowUpState() {
followUpPrompt, followUpPrompt,
followUpImagePaths, followUpImagePaths,
followUpPreviewMap, followUpPreviewMap,
followUpPromptHistory,
// Setters // Setters
setShowFollowUpDialog, setShowFollowUpDialog,
setFollowUpFeature, setFollowUpFeature,
setFollowUpPrompt, setFollowUpPrompt,
setFollowUpImagePaths, setFollowUpImagePaths,
setFollowUpPreviewMap, setFollowUpPreviewMap,
setFollowUpPromptHistory,
// Helpers // Helpers
resetFollowUpState, resetFollowUpState,
handleFollowUpDialogChange, handleFollowUpDialogChange,
addToPromptHistory,
}; };
} }

View File

@@ -0,0 +1,154 @@
import { useState } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Sparkles, ChevronDown, ChevronRight } from 'lucide-react';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import { EnhancementMode, ENHANCEMENT_MODE_LABELS } from './enhancement-constants';
const logger = createLogger('EnhanceWithAI');
interface EnhanceWithAIProps {
/** Current text value to enhance */
value: string;
/** Callback when text is enhanced */
onChange: (enhancedText: string) => void;
/** Optional callback to track enhancement in history */
onHistoryAdd?: (entry: { mode: EnhancementMode; enhancedText: string }) => void;
/** Disable the enhancement feature */
disabled?: boolean;
/** Additional CSS classes */
className?: string;
}
/**
* Reusable "Enhance with AI" component
*
* Provides AI-powered text enhancement with multiple modes:
* - Improve Clarity
* - Add Technical Details
* - Simplify
* - Add Acceptance Criteria
* - User Experience
*
* Used in Add Feature, Edit Feature, and Follow-Up dialogs.
*/
export function EnhanceWithAI({
value,
onChange,
onHistoryAdd,
disabled = false,
className,
}: EnhanceWithAIProps) {
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState<EnhancementMode>('improve');
const [enhanceOpen, setEnhanceOpen] = useState(false);
// Enhancement model override
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
const handleEnhance = async () => {
if (!value.trim() || isEnhancing || disabled) return;
setIsEnhancing(true);
try {
const api = getElectronAPI();
const result = await api.enhancePrompt?.enhance(
value,
enhancementMode,
enhancementOverride.effectiveModel
);
if (result?.success && result.enhancedText) {
const enhancedText = result.enhancedText;
onChange(enhancedText);
// Track in history if callback provided
onHistoryAdd?.({ mode: enhancementMode, enhancedText });
toast.success('Enhanced successfully!');
} else {
toast.error(result?.error || 'Failed to enhance');
}
} catch (error) {
logger.error('Enhancement failed:', error);
toast.error('Failed to enhance');
} finally {
setIsEnhancing(false);
}
};
return (
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen} className={className}>
<CollapsibleTrigger asChild>
<button
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1"
disabled={disabled}
>
{enhanceOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
<Sparkles className="w-4 h-4" />
<span>Enhance with AI</span>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="pt-3">
<div className="flex flex-wrap items-center gap-2 pl-6">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 text-xs" disabled={disabled}>
{ENHANCEMENT_MODE_LABELS[enhancementMode]}
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => setEnhancementMode('improve')}>
{ENHANCEMENT_MODE_LABELS.improve}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('technical')}>
{ENHANCEMENT_MODE_LABELS.technical}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('simplify')}>
{ENHANCEMENT_MODE_LABELS.simplify}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('acceptance')}>
{ENHANCEMENT_MODE_LABELS.acceptance}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('ux-reviewer')}>
{ENHANCEMENT_MODE_LABELS['ux-reviewer']}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant="default"
size="sm"
className="h-8 text-xs"
onClick={handleEnhance}
disabled={!value.trim() || isEnhancing || disabled}
loading={isEnhancing}
>
<Sparkles className="w-3 h-3 mr-1" />
Enhance
</Button>
<ModelOverrideTrigger
currentModelEntry={enhancementOverride.effectiveModelEntry}
onModelChange={enhancementOverride.setOverride}
phase="enhancementModel"
isOverridden={enhancementOverride.isOverridden}
size="sm"
variant="icon"
/>
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -0,0 +1,20 @@
/** Enhancement mode options for AI-powered prompt improvement */
export type EnhancementMode = 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer';
/** Labels for enhancement modes displayed in the UI */
export const ENHANCEMENT_MODE_LABELS: Record<EnhancementMode, string> = {
improve: 'Improve Clarity',
technical: 'Add Technical Details',
simplify: 'Simplify',
acceptance: 'Add Acceptance Criteria',
'ux-reviewer': 'User Experience',
};
/** Descriptions for enhancement modes (for tooltips/accessibility) */
export const ENHANCEMENT_MODE_DESCRIPTIONS: Record<EnhancementMode, string> = {
improve: 'Make the prompt clearer and more concise',
technical: 'Add implementation details and specifications',
simplify: 'Reduce complexity while keeping the core intent',
acceptance: 'Add specific acceptance criteria and test cases',
'ux-reviewer': 'Add user experience considerations and flows',
};

View File

@@ -0,0 +1,129 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { History } from 'lucide-react';
import { toast } from 'sonner';
import { EnhancementMode, ENHANCEMENT_MODE_LABELS } from './enhancement-constants';
/**
* Base interface for history entries
*/
export interface BaseHistoryEntry {
timestamp: string;
source: 'initial' | 'enhance' | 'edit';
enhancementMode?: EnhancementMode;
}
interface EnhancementHistoryButtonProps<T extends BaseHistoryEntry> {
/** Array of history entries */
history: T[];
/** Current value to compare against for highlighting */
currentValue: string;
/** Callback when a history entry is restored */
onRestore: (value: string) => void;
/** Function to extract the text value from a history entry */
valueAccessor: (entry: T) => string;
/** Title for the history popover (e.g., "Version History", "Prompt History") */
title?: string;
/** Message shown when restoring an entry */
restoreMessage?: string;
}
/**
* Reusable history button component for enhancement-related history
*
* Displays a popover with a list of historical versions that can be restored.
* Used in edit-feature-dialog and follow-up-dialog for description/prompt history.
*/
export function EnhancementHistoryButton<T extends BaseHistoryEntry>({
history,
currentValue,
onRestore,
valueAccessor,
title = 'Version History',
restoreMessage = 'Restored from history',
}: EnhancementHistoryButtonProps<T>) {
const [showHistory, setShowHistory] = useState(false);
if (history.length === 0) {
return null;
}
const getSourceLabel = (entry: T): string => {
if (entry.source === 'initial') {
return 'Original';
}
if (entry.source === 'enhance') {
return `Enhanced (${ENHANCEMENT_MODE_LABELS[entry.enhancementMode ?? 'improve']})`;
}
return 'Edited';
};
const formatDate = (timestamp: string): string => {
const date = new Date(timestamp);
return date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<Popover open={showHistory} onOpenChange={setShowHistory}>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1.5 text-xs text-muted-foreground"
>
<History className="w-3.5 h-3.5" />
History ({history.length})
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="p-3 border-b">
<h4 className="font-medium text-sm">{title}</h4>
<p className="text-xs text-muted-foreground mt-1">Click a version to restore it</p>
</div>
<div className="max-h-64 overflow-y-auto p-2 space-y-1">
{[...history].reverse().map((entry, index) => {
const value = valueAccessor(entry);
const isCurrentVersion = value === currentValue;
const sourceLabel = getSourceLabel(entry);
const formattedDate = formatDate(entry.timestamp);
return (
<button
key={`${entry.timestamp}-${index}`}
onClick={() => {
onRestore(value);
setShowHistory(false);
toast.success(restoreMessage);
}}
className={`w-full text-left p-2 rounded-md hover:bg-muted transition-colors ${
isCurrentVersion ? 'bg-muted/50 border border-primary/20' : ''
}`}
>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium">{sourceLabel}</span>
<span className="text-xs text-muted-foreground">{formattedDate}</span>
</div>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
{value.slice(0, 100)}
{value.length > 100 ? '...' : ''}
</p>
{isCurrentVersion && (
<span className="text-xs text-primary font-medium mt-1 block">
Current version
</span>
)}
</button>
);
})}
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,3 @@
export * from './enhancement-constants';
export * from './enhance-with-ai';
export * from './enhancement-history-button';

View File

@@ -10,3 +10,4 @@ export * from './planning-mode-selector';
export * from './planning-mode-select'; export * from './planning-mode-select';
export * from './ancestor-context-section'; export * from './ancestor-context-section';
export * from './work-mode-selector'; export * from './work-mode-selector';
export * from './enhancement';