mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -161,12 +161,14 @@ export function BoardView() {
|
||||
followUpPrompt,
|
||||
followUpImagePaths,
|
||||
followUpPreviewMap,
|
||||
followUpPromptHistory,
|
||||
setShowFollowUpDialog,
|
||||
setFollowUpFeature,
|
||||
setFollowUpPrompt,
|
||||
setFollowUpImagePaths,
|
||||
setFollowUpPreviewMap,
|
||||
handleFollowUpDialogChange,
|
||||
addToPromptHistory,
|
||||
} = useFollowUpState();
|
||||
|
||||
// Selection mode hook for mass editing
|
||||
@@ -1422,6 +1424,8 @@ export function BoardView() {
|
||||
onPreviewMapChange={setFollowUpPreviewMap}
|
||||
onSend={handleSendFollowUp}
|
||||
isMaximized={isMaximized}
|
||||
promptHistory={followUpPromptHistory}
|
||||
onHistoryAdd={addToPromptHistory}
|
||||
/>
|
||||
|
||||
{/* Backlog Plan Dialog */}
|
||||
|
||||
@@ -21,11 +21,9 @@ import {
|
||||
FeatureTextFilePath as DescriptionTextFilePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { Sparkles, ChevronDown, ChevronRight, Play, Cpu, FolderKanban } from 'lucide-react';
|
||||
import { Play, Cpu, FolderKanban } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
useAppStore,
|
||||
@@ -43,16 +41,12 @@ import {
|
||||
WorkModeSelector,
|
||||
PlanningModeSelect,
|
||||
AncestorContextSection,
|
||||
EnhanceWithAI,
|
||||
EnhancementHistoryButton,
|
||||
type BaseHistoryEntry,
|
||||
} from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
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 {
|
||||
getAncestors,
|
||||
@@ -139,11 +133,12 @@ export function AddFeatureDialog({
|
||||
// UI state
|
||||
const [previewMap, setPreviewMap] = useState<ImagePreviewMap>(() => new Map());
|
||||
const [descriptionError, setDescriptionError] = useState(false);
|
||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||
const [enhancementMode, setEnhancementMode] = useState<
|
||||
'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'
|
||||
>('improve');
|
||||
const [enhanceOpen, setEnhanceOpen] = useState(false);
|
||||
|
||||
// Description history state
|
||||
interface DescriptionHistoryEntry extends BaseHistoryEntry {
|
||||
description: string;
|
||||
}
|
||||
const [descriptionHistory, setDescriptionHistory] = useState<DescriptionHistoryEntry[]>([]);
|
||||
|
||||
// Spawn mode state
|
||||
const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
|
||||
@@ -152,9 +147,6 @@ export function AddFeatureDialog({
|
||||
// Get defaults from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval } = useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
|
||||
// Track previous open state to detect when dialog opens
|
||||
const wasOpenRef = useRef(false);
|
||||
|
||||
@@ -171,6 +163,9 @@ export function AddFeatureDialog({
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
setModelEntry({ model: 'opus' });
|
||||
|
||||
// Initialize description history (empty for new feature)
|
||||
setDescriptionHistory([]);
|
||||
|
||||
// Initialize ancestors for spawn mode
|
||||
if (parentFeature) {
|
||||
const ancestorList = getAncestors(parentFeature, allFeatures);
|
||||
@@ -279,7 +274,7 @@ export function AddFeatureDialog({
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
setPreviewMap(new Map());
|
||||
setDescriptionError(false);
|
||||
setEnhanceOpen(false);
|
||||
setDescriptionHistory([]);
|
||||
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
|
||||
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';
|
||||
@@ -380,7 +348,18 @@ export function AddFeatureDialog({
|
||||
{/* Task Details Section */}
|
||||
<div className={cardClass}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<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
|
||||
value={description}
|
||||
onChange={(value) => {
|
||||
@@ -409,75 +388,22 @@ export function AddFeatureDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Collapsible Enhancement Section */}
|
||||
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1">
|
||||
{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">
|
||||
{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>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{/* Enhancement Section */}
|
||||
<EnhanceWithAI
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
onHistoryAdd={({ mode, enhancedText }) => {
|
||||
setDescriptionHistory((prev) => [
|
||||
...prev,
|
||||
{
|
||||
description: enhancedText,
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'enhance',
|
||||
enhancementMode: mode,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AI & Execution Section */}
|
||||
|
||||
@@ -21,18 +21,8 @@ import {
|
||||
FeatureTextFilePath as DescriptionTextFilePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import {
|
||||
Sparkles,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
GitBranch,
|
||||
History,
|
||||
Cpu,
|
||||
FolderKanban,
|
||||
} from 'lucide-react';
|
||||
import { GitBranch, Cpu, FolderKanban } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { cn, modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
|
||||
import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '@automaker/types';
|
||||
@@ -41,17 +31,11 @@ import {
|
||||
PrioritySelector,
|
||||
WorkModeSelector,
|
||||
PlanningModeSelect,
|
||||
EnhanceWithAI,
|
||||
EnhancementHistoryButton,
|
||||
} from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
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 { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
|
||||
@@ -110,11 +94,6 @@ export function EditFeatureDialog({
|
||||
const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>(
|
||||
() => 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 [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip');
|
||||
const [requirePlanApproval, setRequirePlanApproval] = useState(
|
||||
@@ -137,11 +116,6 @@ export function EditFeatureDialog({
|
||||
>(null);
|
||||
// Track the original description when the dialog opened for comparison
|
||||
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(() => {
|
||||
setEditingFeature(feature);
|
||||
@@ -153,8 +127,6 @@ export function EditFeatureDialog({
|
||||
// Reset history tracking state
|
||||
setOriginalDescription(feature.description ?? '');
|
||||
setDescriptionChangeSource(null);
|
||||
setShowHistory(false);
|
||||
setEnhanceOpen(false);
|
||||
// Reset model entry
|
||||
setModelEntry({
|
||||
model: (feature.model as ModelAlias) || 'opus',
|
||||
@@ -164,7 +136,6 @@ export function EditFeatureDialog({
|
||||
} else {
|
||||
setEditFeaturePreviewMap(new Map());
|
||||
setDescriptionChangeSource(null);
|
||||
setShowHistory(false);
|
||||
}
|
||||
}, [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) {
|
||||
return null;
|
||||
}
|
||||
@@ -306,92 +247,17 @@ export function EditFeatureDialog({
|
||||
<Label htmlFor="edit-description">Description</Label>
|
||||
{/* Version History Button */}
|
||||
{feature?.descriptionHistory && feature.descriptionHistory.length > 0 && (
|
||||
<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 ({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');
|
||||
setShowHistory(false);
|
||||
toast.success('Description restored from history');
|
||||
}}
|
||||
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">
|
||||
{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>
|
||||
<EnhancementHistoryButton
|
||||
history={feature.descriptionHistory}
|
||||
currentValue={editingFeature.description}
|
||||
onRestore={(description) => {
|
||||
setEditingFeature((prev) => (prev ? { ...prev, description } : prev));
|
||||
setDescriptionChangeSource('edit');
|
||||
}}
|
||||
valueAccessor={(entry) => entry.description}
|
||||
title="Version History"
|
||||
restoreMessage="Description restored from history"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DescriptionImageDropZone
|
||||
@@ -443,75 +309,14 @@ export function EditFeatureDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Collapsible Enhancement Section */}
|
||||
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1">
|
||||
{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">
|
||||
{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>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{/* Enhancement Section */}
|
||||
<EnhanceWithAI
|
||||
value={editingFeature.description}
|
||||
onChange={(enhanced) =>
|
||||
setEditingFeature((prev) => (prev ? { ...prev, description: enhanced } : prev))
|
||||
}
|
||||
onHistoryAdd={({ mode }) => setDescriptionChangeSource({ source: 'enhance', mode })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AI & Execution Section */}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -17,6 +18,19 @@ import {
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
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 {
|
||||
open: boolean;
|
||||
@@ -30,6 +44,10 @@ interface FollowUpDialogProps {
|
||||
onPreviewMapChange: (map: ImagePreviewMap) => void;
|
||||
onSend: () => void;
|
||||
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({
|
||||
@@ -44,9 +62,11 @@ export function FollowUpDialog({
|
||||
onPreviewMapChange,
|
||||
onSend,
|
||||
isMaximized,
|
||||
promptHistory = [],
|
||||
onHistoryAdd,
|
||||
}: FollowUpDialogProps) {
|
||||
const handleClose = (open: boolean) => {
|
||||
if (!open) {
|
||||
const handleClose = (openState: boolean) => {
|
||||
if (!openState) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
@@ -77,7 +97,18 @@ export function FollowUpDialog({
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="follow-up-prompt">Instructions</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<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
|
||||
value={prompt}
|
||||
onChange={onPromptChange}
|
||||
@@ -88,6 +119,21 @@ export function FollowUpDialog({
|
||||
onPreviewMapChange={onPreviewMapChange}
|
||||
/>
|
||||
</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">
|
||||
The agent will continue from where it left off, using the existing context. You can
|
||||
attach screenshots to help explain the issue.
|
||||
|
||||
@@ -5,6 +5,6 @@ export { CompletedFeaturesModal } from './completed-features-modal';
|
||||
export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog';
|
||||
export { DeleteCompletedFeatureDialog } from './delete-completed-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 { MassEditDialog } from './mass-edit-dialog';
|
||||
|
||||
@@ -4,13 +4,18 @@ import {
|
||||
FeatureImagePath as DescriptionImagePath,
|
||||
ImagePreviewMap,
|
||||
} 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() {
|
||||
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
|
||||
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
|
||||
const [followUpPrompt, setFollowUpPrompt] = useState('');
|
||||
const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]);
|
||||
const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(() => new Map());
|
||||
const [followUpPromptHistory, setFollowUpPromptHistory] = useState<FollowUpHistoryEntry[]>([]);
|
||||
|
||||
const resetFollowUpState = useCallback(() => {
|
||||
setShowFollowUpDialog(false);
|
||||
@@ -18,6 +23,7 @@ export function useFollowUpState() {
|
||||
setFollowUpPrompt('');
|
||||
setFollowUpImagePaths([]);
|
||||
setFollowUpPreviewMap(new Map());
|
||||
setFollowUpPromptHistory([]);
|
||||
}, []);
|
||||
|
||||
const handleFollowUpDialogChange = useCallback(
|
||||
@@ -31,6 +37,13 @@ export function useFollowUpState() {
|
||||
[resetFollowUpState]
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds a new entry to the prompt history
|
||||
*/
|
||||
const addToPromptHistory = useCallback((entry: FollowUpHistoryEntry) => {
|
||||
setFollowUpPromptHistory((prev) => [...prev, entry]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
showFollowUpDialog,
|
||||
@@ -38,14 +51,17 @@ export function useFollowUpState() {
|
||||
followUpPrompt,
|
||||
followUpImagePaths,
|
||||
followUpPreviewMap,
|
||||
followUpPromptHistory,
|
||||
// Setters
|
||||
setShowFollowUpDialog,
|
||||
setFollowUpFeature,
|
||||
setFollowUpPrompt,
|
||||
setFollowUpImagePaths,
|
||||
setFollowUpPreviewMap,
|
||||
setFollowUpPromptHistory,
|
||||
// Helpers
|
||||
resetFollowUpState,
|
||||
handleFollowUpDialogChange,
|
||||
addToPromptHistory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './enhancement-constants';
|
||||
export * from './enhance-with-ai';
|
||||
export * from './enhancement-history-button';
|
||||
@@ -10,3 +10,4 @@ export * from './planning-mode-selector';
|
||||
export * from './planning-mode-select';
|
||||
export * from './ancestor-context-section';
|
||||
export * from './work-mode-selector';
|
||||
export * from './enhancement';
|
||||
|
||||
Reference in New Issue
Block a user