Merge pull request #123 from AutoMaker-Org/enhance-feature-with-ai

feat: add AI enhancement feature to settings and board views
This commit is contained in:
Web Dev Cody
2025-12-16 02:01:30 -05:00
committed by GitHub
15 changed files with 1211 additions and 8 deletions

View File

@@ -19,7 +19,9 @@ import {
FeatureImagePath as DescriptionImagePath,
ImagePreviewMap,
} from "@/components/ui/description-image-dropzone";
import { MessageSquare, Settings2, FlaskConical } from "lucide-react";
import { MessageSquare, Settings2, FlaskConical, Sparkles, ChevronDown } from "lucide-react";
import { toast } from "sonner";
import { getElectronAPI } from "@/lib/electron";
import { modelSupportsThinking } from "@/lib/utils";
import {
useAppStore,
@@ -34,6 +36,12 @@ import {
ProfileQuickSelect,
TestingTabContent,
} from "../shared";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface AddFeatureDialogProps {
open: boolean;
@@ -79,6 +87,11 @@ export function AddFeatureDialog({
useState<ImagePreviewMap>(() => new Map());
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [descriptionError, setDescriptionError] = useState(false);
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
// Get enhancement model from store
const { enhancementModel } = useAppStore();
// Sync skipTests default when dialog opens
useEffect(() => {
@@ -139,6 +152,33 @@ export function AddFeatureDialog({
}
};
const handleEnhanceDescription = async () => {
if (!newFeature.description.trim() || isEnhancing) return;
setIsEnhancing(true);
try {
const api = getElectronAPI();
const result = await api.enhancePrompt?.enhance(
newFeature.description,
enhancementMode,
enhancementModel
);
if (result?.success && result.enhancedText) {
const enhancedText = result.enhancedText;
setNewFeature(prev => ({ ...prev, description: enhancedText }));
toast.success("Description enhanced!");
} else {
toast.error(result?.error || "Failed to enhance description");
}
} catch (error) {
console.error("Enhancement failed:", error);
toast.error("Failed to enhance description");
} finally {
setIsEnhancing(false);
}
};
const handleModelSelect = (model: AgentModel) => {
setNewFeature({
...newFeature,
@@ -203,7 +243,7 @@ export function AddFeatureDialog({
</TabsList>
{/* Prompt Tab */}
<TabsContent value="prompt" className="space-y-4 overflow-y-auto">
<TabsContent value="prompt" className="space-y-4 overflow-y-auto cursor-default">
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<DescriptionImageDropZone
@@ -225,6 +265,45 @@ export function AddFeatureDialog({
error={descriptionError}
/>
</div>
<div className="flex w-fit items-center gap-3 select-none cursor-default">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-[180px] justify-between">
{enhancementMode === 'improve' && 'Improve Clarity'}
{enhancementMode === 'technical' && 'Add Technical Details'}
{enhancementMode === 'simplify' && 'Simplify'}
{enhancementMode === 'acceptance' && 'Add Acceptance Criteria'}
<ChevronDown className="w-4 h-4 ml-2" />
</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>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleEnhanceDescription}
disabled={!newFeature.description.trim() || isEnhancing}
loading={isEnhancing}
>
<Sparkles className="w-4 h-4 mr-2" />
Enhance with AI
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category (optional)</Label>
<CategoryAutocomplete
@@ -240,7 +319,7 @@ export function AddFeatureDialog({
</TabsContent>
{/* Model Tab */}
<TabsContent value="model" className="space-y-4 overflow-y-auto">
<TabsContent value="model" className="space-y-4 overflow-y-auto cursor-default">
{/* Show Advanced Options Toggle */}
{showProfilesOnly && (
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
@@ -303,7 +382,7 @@ export function AddFeatureDialog({
</TabsContent>
{/* Testing Tab */}
<TabsContent value="testing" className="space-y-4 overflow-y-auto">
<TabsContent value="testing" className="space-y-4 overflow-y-auto cursor-default">
<TestingTabContent
skipTests={newFeature.skipTests}
onSkipTestsChange={(skipTests) =>

View File

@@ -19,13 +19,16 @@ import {
FeatureImagePath as DescriptionImagePath,
ImagePreviewMap,
} from "@/components/ui/description-image-dropzone";
import { MessageSquare, Settings2, FlaskConical } from "lucide-react";
import { MessageSquare, Settings2, FlaskConical, Sparkles, ChevronDown } from "lucide-react";
import { toast } from "sonner";
import { getElectronAPI } from "@/lib/electron";
import { modelSupportsThinking } from "@/lib/utils";
import {
Feature,
AgentModel,
ThinkingLevel,
AIProfile,
useAppStore,
} from "@/store/app-store";
import {
ModelSelector,
@@ -33,6 +36,12 @@ import {
ProfileQuickSelect,
TestingTabContent,
} from "../shared";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface EditFeatureDialogProps {
feature: Feature | null;
@@ -68,6 +77,11 @@ export function EditFeatureDialog({
const [editFeaturePreviewMap, setEditFeaturePreviewMap] =
useState<ImagePreviewMap>(() => new Map());
const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false);
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
// Get enhancement model from store
const { enhancementModel } = useAppStore();
useEffect(() => {
setEditingFeature(feature);
@@ -127,6 +141,33 @@ 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,
enhancementModel
);
if (result?.success && result.enhancedText) {
const enhancedText = result.enhancedText;
setEditingFeature(prev => prev ? { ...prev, description: enhancedText } : prev);
toast.success("Description enhanced!");
} else {
toast.error(result?.error || "Failed to enhance description");
}
} catch (error) {
console.error("Enhancement failed:", error);
toast.error("Failed to enhance description");
} finally {
setIsEnhancing(false);
}
};
const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model);
if (!editingFeature) {
@@ -175,7 +216,7 @@ export function EditFeatureDialog({
</TabsList>
{/* Prompt Tab */}
<TabsContent value="prompt" className="space-y-4 overflow-y-auto">
<TabsContent value="prompt" className="space-y-4 overflow-y-auto cursor-default">
<div className="space-y-2">
<Label htmlFor="edit-description">Description</Label>
<DescriptionImageDropZone
@@ -199,6 +240,45 @@ export function EditFeatureDialog({
data-testid="edit-feature-description"
/>
</div>
<div className="flex w-fit items-center gap-3 select-none cursor-default">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-[180px] justify-between">
{enhancementMode === 'improve' && 'Improve Clarity'}
{enhancementMode === 'technical' && 'Add Technical Details'}
{enhancementMode === 'simplify' && 'Simplify'}
{enhancementMode === 'acceptance' && 'Add Acceptance Criteria'}
<ChevronDown className="w-4 h-4 ml-2" />
</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>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleEnhanceDescription}
disabled={!editingFeature.description.trim() || isEnhancing}
loading={isEnhancing}
>
<Sparkles className="w-4 h-4 mr-2" />
Enhance with AI
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="edit-category">Category (optional)</Label>
<CategoryAutocomplete
@@ -217,7 +297,7 @@ export function EditFeatureDialog({
</TabsContent>
{/* Model Tab */}
<TabsContent value="model" className="space-y-4 overflow-y-auto">
<TabsContent value="model" className="space-y-4 overflow-y-auto cursor-default">
{/* Show Advanced Options Toggle */}
{showProfilesOnly && (
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
@@ -283,7 +363,7 @@ export function EditFeatureDialog({
</TabsContent>
{/* Testing Tab */}
<TabsContent value="testing" className="space-y-4 overflow-y-auto">
<TabsContent value="testing" className="space-y-4 overflow-y-auto cursor-default">
<TestingTabContent
skipTests={editingFeature.skipTests ?? false}
onSkipTestsChange={(skipTests) =>

View File

@@ -11,6 +11,7 @@ import { DeleteProjectDialog } from "./settings-view/components/delete-project-d
import { SettingsNavigation } from "./settings-view/components/settings-navigation";
import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
import { AIEnhancementSection } from "./settings-view/ai-enhancement";
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
import { AudioSection } from "./settings-view/audio/audio-section";
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
@@ -89,6 +90,8 @@ export function SettingsView() {
onRefresh={handleRefreshClaudeCli}
/>
);
case "ai-enhancement":
return <AIEnhancementSection />;
case "appearance":
return (
<AppearanceSection

View File

@@ -0,0 +1,91 @@
import { Label } from "@/components/ui/label";
import { Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import { useAppStore } from "@/store/app-store";
import { CLAUDE_MODELS } from "@/components/views/board-view/shared/model-constants";
export function AIEnhancementSection() {
const { enhancementModel, setEnhancementModel } = useAppStore();
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Sparkles className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">AI Enhancement</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Choose the model used when enhancing feature descriptions.
</p>
</div>
<div className="p-6 space-y-4">
<div className="space-y-4">
<Label className="text-foreground font-medium">
Enhancement Model
</Label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{CLAUDE_MODELS.map(({ id, label, description, badge }) => {
const isActive = enhancementModel === id;
return (
<button
key={id}
onClick={() => setEnhancementModel(id)}
className={cn(
"group flex flex-col items-start gap-2 px-4 py-4 rounded-xl text-left",
"transition-all duration-200 ease-out",
isActive
? [
"bg-gradient-to-br from-brand-500/15 to-brand-600/10",
"border-2 border-brand-500/40",
"text-foreground",
"shadow-md shadow-brand-500/10",
]
: [
"bg-accent/30 hover:bg-accent/50",
"border border-border/50 hover:border-border",
"text-muted-foreground hover:text-foreground",
"hover:shadow-sm",
],
"hover:scale-[1.02] active:scale-[0.98]"
)}
data-testid={`enhancement-model-${id}`}
>
<div className="flex items-center gap-2 w-full">
<span className={cn(
"font-medium text-sm",
isActive ? "text-foreground" : "group-hover:text-foreground"
)}>
{label}
</span>
{badge && (
<span className={cn(
"ml-auto text-xs px-2 py-0.5 rounded-full",
isActive
? "bg-brand-500/20 text-brand-500"
: "bg-accent text-muted-foreground"
)}>
{badge}
</span>
)}
</div>
<span className="text-xs text-muted-foreground/80">
{description}
</span>
</button>
);
})}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export { AIEnhancementSection } from "./ai-enhancement-section";

View File

@@ -7,6 +7,7 @@ import {
Volume2,
FlaskConical,
Trash2,
Sparkles,
} from "lucide-react";
import type { SettingsViewId } from "../hooks/use-settings-view";
@@ -20,6 +21,7 @@ export interface NavigationItem {
export const NAV_ITEMS: NavigationItem[] = [
{ id: "api-keys", label: "API Keys", icon: Key },
{ id: "claude", label: "Claude", icon: Terminal },
{ id: "ai-enhancement", label: "AI Enhancement", icon: Sparkles },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
{ id: "audio", label: "Audio", icon: Volume2 },

View File

@@ -3,6 +3,7 @@ import { useState, useCallback } from "react";
export type SettingsViewId =
| "api-keys"
| "claude"
| "ai-enhancement"
| "appearance"
| "keyboard"
| "audio"

View File

@@ -316,6 +316,13 @@ export interface ElectronAPI {
autoMode?: AutoModeAPI;
features?: FeaturesAPI;
runningAgents?: RunningAgentsAPI;
enhancePrompt?: {
enhance: (originalText: string, enhancementMode: string, model?: string) => Promise<{
success: boolean;
enhancedText?: string;
error?: string;
}>;
};
setup?: {
getClaudeStatus: () => Promise<{
success: boolean;

View File

@@ -58,6 +58,12 @@ type EventType =
type EventCallback = (payload: unknown) => void;
interface EnhancePromptResult {
success: boolean;
enhancedText?: string;
error?: string;
}
/**
* HTTP API Client that implements ElectronAPI interface
*/
@@ -546,6 +552,20 @@ export class HttpApiClient implements ElectronAPI {
},
};
// Enhance Prompt API
enhancePrompt = {
enhance: (
originalText: string,
enhancementMode: string,
model?: string
): Promise<EnhancePromptResult> =>
this.post("/api/enhance-prompt", {
originalText,
enhancementMode,
model,
}),
};
// Worktree API
worktree: WorktreeAPI = {
revertFeature: (projectPath: string, featureId: string) =>

View File

@@ -409,6 +409,9 @@ export interface AppState {
// Audio Settings
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
// Enhancement Model Settings
enhancementModel: AgentModel; // Model used for feature enhancement (default: sonnet)
// Project Analysis
projectAnalysis: ProjectAnalysis | null;
isAnalyzing: boolean;
@@ -577,6 +580,9 @@ export interface AppActions {
// Audio Settings actions
setMuteDoneSound: (muted: boolean) => void;
// Enhancement Model actions
setEnhancementModel: (model: AgentModel) => void;
// AI Profile actions
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
@@ -714,6 +720,7 @@ const initialState: AppState = {
showProfilesOnly: false, // Default to showing all options (not profiles only)
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
muteDoneSound: false, // Default to sound enabled (not muted)
enhancementModel: "sonnet", // Default to sonnet for feature enhancement
aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null,
isAnalyzing: false,
@@ -1331,6 +1338,9 @@ export const useAppStore = create<AppState & AppActions>()(
// Audio Settings actions
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
// Enhancement Model actions
setEnhancementModel: (model) => set({ enhancementModel: model }),
// AI Profile actions
addAIProfile: (profile) => {
const id = `profile-${Date.now()}-${Math.random()
@@ -2162,6 +2172,7 @@ export const useAppStore = create<AppState & AppActions>()(
showProfilesOnly: state.showProfilesOnly,
keyboardShortcuts: state.keyboardShortcuts,
muteDoneSound: state.muteDoneSound,
enhancementModel: state.enhancementModel,
// Profiles and sessions
aiProfiles: state.aiProfiles,
chatSessions: state.chatSessions,