mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +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,
|
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 */}
|
||||||
|
|||||||
@@ -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">
|
||||||
<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
|
<DescriptionImageDropZone
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -409,75 +388,22 @@ 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>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI & Execution Section */}
|
{/* AI & Execution Section */}
|
||||||
|
|||||||
@@ -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"
|
setDescriptionChangeSource('edit');
|
||||||
className="h-7 gap-1.5 text-xs text-muted-foreground"
|
}}
|
||||||
>
|
valueAccessor={(entry) => entry.description}
|
||||||
<History className="w-3.5 h-3.5" />
|
title="Version History"
|
||||||
History ({feature.descriptionHistory.length})
|
restoreMessage="Description restored from history"
|
||||||
</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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DescriptionImageDropZone
|
<DescriptionImageDropZone
|
||||||
@@ -443,75 +309,14 @@ 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>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI & Execution Section */}
|
{/* AI & Execution Section */}
|
||||||
|
|||||||
@@ -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">
|
||||||
<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
|
<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.
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 './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';
|
||||||
|
|||||||
Reference in New Issue
Block a user