mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
feat: refactor spec-view to folder pattern and add feature count selector
Closes #151 - Refactor spec-view.tsx from 1,230 lines to ~170 lines following folder-pattern.md - Create unified CreateSpecDialog with all features from both dialogs: - featureCount selector (20/50/100) - was missing in spec-view - analyzeProject checkbox - was missing in sidebar - Extract components: spec-header, spec-editor, spec-empty-state - Extract hooks: use-spec-loading, use-spec-save, use-spec-generation - Extract dialogs: create-spec-dialog, regenerate-spec-dialog - Update sidebar to use new CreateSpecDialog with analyzeProject state - Delete deprecated project-setup-dialog.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,10 +81,8 @@ import { themeOptions } from "@/config/theme-options";
|
||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
||||
import { NewProjectModal } from "@/components/new-project-modal";
|
||||
import {
|
||||
ProjectSetupDialog,
|
||||
type FeatureCount,
|
||||
} from "@/components/layout/project-setup-dialog";
|
||||
import { CreateSpecDialog } from "@/components/views/spec-view/dialogs";
|
||||
import type { FeatureCount } from "@/components/views/spec-view/types";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
@@ -290,6 +288,7 @@ export function Sidebar() {
|
||||
const [setupProjectPath, setSetupProjectPath] = useState("");
|
||||
const [projectOverview, setProjectOverview] = useState("");
|
||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||
const [analyzeProject, setAnalyzeProject] = useState(true);
|
||||
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
|
||||
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
||||
|
||||
@@ -496,7 +495,7 @@ export function Sidebar() {
|
||||
setupProjectPath,
|
||||
projectOverview.trim(),
|
||||
generateFeatures,
|
||||
undefined, // analyzeProject - use default
|
||||
analyzeProject,
|
||||
generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features
|
||||
);
|
||||
|
||||
@@ -525,6 +524,7 @@ export function Sidebar() {
|
||||
setupProjectPath,
|
||||
projectOverview,
|
||||
generateFeatures,
|
||||
analyzeProject,
|
||||
featureCount,
|
||||
setSpecCreatingForProject,
|
||||
]);
|
||||
@@ -2276,18 +2276,23 @@ export function Sidebar() {
|
||||
</Dialog>
|
||||
|
||||
{/* New Project Setup Dialog */}
|
||||
<ProjectSetupDialog
|
||||
<CreateSpecDialog
|
||||
open={showSetupDialog}
|
||||
onOpenChange={setShowSetupDialog}
|
||||
projectOverview={projectOverview}
|
||||
onProjectOverviewChange={setProjectOverview}
|
||||
generateFeatures={generateFeatures}
|
||||
onGenerateFeaturesChange={setGenerateFeatures}
|
||||
analyzeProject={analyzeProject}
|
||||
onAnalyzeProjectChange={setAnalyzeProject}
|
||||
featureCount={featureCount}
|
||||
onFeatureCountChange={setFeatureCount}
|
||||
onCreateSpec={handleCreateInitialSpec}
|
||||
onSkip={handleSkipSetup}
|
||||
isCreatingSpec={isCreatingSpec}
|
||||
showSkipButton={true}
|
||||
title="Set Up Your Project"
|
||||
description="We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt to help describe your project for our system. We'll analyze your project's tech stack and create a comprehensive specification."
|
||||
/>
|
||||
|
||||
{/* New Project Onboarding Dialog */}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
export { SpecHeader } from "./spec-header";
|
||||
export { SpecEditor } from "./spec-editor";
|
||||
export { SpecEmptyState } from "./spec-empty-state";
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
|
||||
|
||||
interface SpecEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SpecEditor({ value, onChange }: SpecEditorProps) {
|
||||
return (
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<Card className="h-full">
|
||||
<XmlSyntaxEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Write your app specification here..."
|
||||
data-testid="spec-editor"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
FileText,
|
||||
FilePlus2,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { PHASE_LABELS } from "../constants";
|
||||
|
||||
interface SpecEmptyStateProps {
|
||||
projectPath: string;
|
||||
isCreating: boolean;
|
||||
isRegenerating: boolean;
|
||||
currentPhase: string;
|
||||
errorMessage: string;
|
||||
onCreateClick: () => void;
|
||||
}
|
||||
|
||||
export function SpecEmptyState({
|
||||
projectPath,
|
||||
isCreating,
|
||||
isRegenerating,
|
||||
currentPhase,
|
||||
errorMessage,
|
||||
onCreateClick,
|
||||
}: SpecEmptyStateProps) {
|
||||
const isProcessing = isCreating || isRegenerating;
|
||||
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="spec-view-empty"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">App Specification</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{projectPath}/.automaker/app_spec.txt
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{isProcessing && (
|
||||
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-linear-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md">
|
||||
<div className="relative">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-primary shrink-0" />
|
||||
<div className="absolute inset-0 w-5 h-5 animate-ping text-primary/20" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||
{isCreating
|
||||
? "Generating Specification"
|
||||
: "Regenerating Specification"}
|
||||
</span>
|
||||
{currentPhase && (
|
||||
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
||||
{phaseLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<span className="text-sm font-medium">Error: {errorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Empty State Content */}
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="p-4 rounded-full bg-primary/10">
|
||||
{isCreating ? (
|
||||
<Loader2 className="w-12 h-12 text-primary animate-spin" />
|
||||
) : (
|
||||
<FilePlus2 className="w-12 h-12 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
{isCreating ? (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<span>Generating App Specification</span>
|
||||
</div>
|
||||
{currentPhase && (
|
||||
<div className="px-6 py-3.5 rounded-xl bg-linear-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md inline-flex items-center justify-center">
|
||||
<span className="text-sm font-semibold text-primary text-center tracking-tight">
|
||||
{phaseLabel}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
"No App Specification Found"
|
||||
)}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
{isCreating
|
||||
? currentPhase === "feature_generation"
|
||||
? "The app specification has been created! Now generating features from the implementation roadmap..."
|
||||
: "We're analyzing your project and generating a comprehensive specification. This may take a few moments..."
|
||||
: "Create an app specification to help our system understand your project. We'll analyze your codebase and generate a comprehensive spec based on your description."}
|
||||
</p>
|
||||
{errorMessage && (
|
||||
<div className="mb-4 p-3 rounded-md bg-destructive/10 border border-destructive/20">
|
||||
<p className="text-sm text-destructive font-medium">Error:</p>
|
||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
{!isCreating && (
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button size="lg" onClick={onCreateClick}>
|
||||
<FilePlus2 className="w-5 h-5 mr-2" />
|
||||
Create app_spec
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Save,
|
||||
Sparkles,
|
||||
Loader2,
|
||||
FileText,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { PHASE_LABELS } from "../constants";
|
||||
|
||||
interface SpecHeaderProps {
|
||||
projectPath: string;
|
||||
isRegenerating: boolean;
|
||||
isCreating: boolean;
|
||||
isGeneratingFeatures: boolean;
|
||||
isSaving: boolean;
|
||||
hasChanges: boolean;
|
||||
currentPhase: string;
|
||||
errorMessage: string;
|
||||
onRegenerateClick: () => void;
|
||||
onSaveClick: () => void;
|
||||
}
|
||||
|
||||
export function SpecHeader({
|
||||
projectPath,
|
||||
isRegenerating,
|
||||
isCreating,
|
||||
isGeneratingFeatures,
|
||||
isSaving,
|
||||
hasChanges,
|
||||
currentPhase,
|
||||
errorMessage,
|
||||
onRegenerateClick,
|
||||
onSaveClick,
|
||||
}: SpecHeaderProps) {
|
||||
const isProcessing = isRegenerating || isCreating || isGeneratingFeatures;
|
||||
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">App Specification</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{projectPath}/.automaker/app_spec.txt
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isProcessing && (
|
||||
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-linear-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md">
|
||||
<div className="relative">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-primary shrink-0" />
|
||||
<div className="absolute inset-0 w-5 h-5 animate-ping text-primary/20" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
|
||||
{isGeneratingFeatures
|
||||
? "Generating Features"
|
||||
: isCreating
|
||||
? "Generating Specification"
|
||||
: "Regenerating Specification"}
|
||||
</span>
|
||||
{currentPhase && (
|
||||
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
|
||||
{phaseLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-linear-to-r from-destructive/15 to-destructive/5 border border-destructive/30 shadow-lg backdrop-blur-md">
|
||||
<AlertCircle className="w-5 h-5 text-destructive shrink-0" />
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<span className="text-sm font-semibold text-destructive leading-tight tracking-tight">
|
||||
Error
|
||||
</span>
|
||||
<span className="text-xs text-destructive/90 leading-tight font-medium">
|
||||
{errorMessage}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onRegenerateClick}
|
||||
disabled={isProcessing}
|
||||
data-testid="regenerate-spec"
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isRegenerating ? "Regenerating..." : "Regenerate"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onSaveClick}
|
||||
disabled={!hasChanges || isSaving || isProcessing}
|
||||
data-testid="save-spec"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
apps/ui/src/components/views/spec-view/constants.ts
Normal file
29
apps/ui/src/components/views/spec-view/constants.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { FeatureCount } from "./types";
|
||||
|
||||
// Delay before reloading spec file to ensure it's written to disk
|
||||
export const SPEC_FILE_WRITE_DELAY = 500;
|
||||
|
||||
// Interval for polling backend status during generation
|
||||
export const STATUS_CHECK_INTERVAL_MS = 2000;
|
||||
|
||||
// Feature count options with labels and warnings
|
||||
export const FEATURE_COUNT_OPTIONS: {
|
||||
value: FeatureCount;
|
||||
label: string;
|
||||
warning?: string;
|
||||
}[] = [
|
||||
{ value: 20, label: "20" },
|
||||
{ value: 50, label: "50", warning: "May take up to 5 minutes" },
|
||||
{ value: 100, label: "100", warning: "May take up to 5 minutes" },
|
||||
];
|
||||
|
||||
// Phase display labels for UI
|
||||
export const PHASE_LABELS: Record<string, string> = {
|
||||
initialization: "Initializing...",
|
||||
setup: "Setting up tools...",
|
||||
analysis: "Analyzing project structure...",
|
||||
spec_complete: "Spec created! Generating features...",
|
||||
feature_generation: "Creating features from roadmap...",
|
||||
complete: "Complete!",
|
||||
error: "Error occurred",
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import { Sparkles, Clock } from "lucide-react";
|
||||
import { Sparkles, Clock, Loader2 } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,66 +8,49 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FEATURE_COUNT_OPTIONS } from "../constants";
|
||||
import type { CreateSpecDialogProps, FeatureCount } from "../types";
|
||||
|
||||
// Feature count options
|
||||
export type FeatureCount = 20 | 50 | 100;
|
||||
const FEATURE_COUNT_OPTIONS: {
|
||||
value: FeatureCount;
|
||||
label: string;
|
||||
warning?: string;
|
||||
}[] = [
|
||||
{ value: 20, label: "20" },
|
||||
{ value: 50, label: "50", warning: "May take up to 5 minutes" },
|
||||
{ value: 100, label: "100", warning: "May take up to 5 minutes" },
|
||||
];
|
||||
|
||||
interface ProjectSetupDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
projectOverview: string;
|
||||
onProjectOverviewChange: (value: string) => void;
|
||||
generateFeatures: boolean;
|
||||
onGenerateFeaturesChange: (value: boolean) => void;
|
||||
featureCount: FeatureCount;
|
||||
onFeatureCountChange: (value: FeatureCount) => void;
|
||||
onCreateSpec: () => void;
|
||||
onSkip: () => void;
|
||||
isCreatingSpec: boolean;
|
||||
}
|
||||
|
||||
export function ProjectSetupDialog({
|
||||
export function CreateSpecDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
projectOverview,
|
||||
onProjectOverviewChange,
|
||||
generateFeatures,
|
||||
onGenerateFeaturesChange,
|
||||
analyzeProject,
|
||||
onAnalyzeProjectChange,
|
||||
featureCount,
|
||||
onFeatureCountChange,
|
||||
onCreateSpec,
|
||||
onSkip,
|
||||
isCreatingSpec,
|
||||
}: ProjectSetupDialogProps) {
|
||||
showSkipButton = false,
|
||||
title = "Create App Specification",
|
||||
description = "We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt to help describe your project for our system. We'll analyze your project's tech stack and create a comprehensive specification.",
|
||||
}: CreateSpecDialogProps) {
|
||||
const selectedOption = FEATURE_COUNT_OPTIONS.find(
|
||||
(o) => o.value === featureCount
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
onOpenChange(open);
|
||||
if (!open && !isCreatingSpec) {
|
||||
if (!open && !isCreatingSpec && onSkip) {
|
||||
onSkip();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Set Up Your Project</DialogTitle>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
We didn't find an app_spec.txt file. Let us help you generate
|
||||
your app_spec.txt to help describe your project for our system.
|
||||
We'll analyze your project's tech stack and create a
|
||||
comprehensive specification.
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -86,21 +68,52 @@ export function ProjectSetupDialog({
|
||||
onChange={(e) => onProjectOverviewChange(e.target.value)}
|
||||
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
|
||||
autoFocus
|
||||
disabled={isCreatingSpec}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 pt-2">
|
||||
<Checkbox
|
||||
id="sidebar-generate-features"
|
||||
id="create-analyze-project"
|
||||
checked={analyzeProject}
|
||||
onCheckedChange={(checked) =>
|
||||
onAnalyzeProjectChange(checked === true)
|
||||
}
|
||||
disabled={isCreatingSpec}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="create-analyze-project"
|
||||
className={`text-sm font-medium ${
|
||||
isCreatingSpec ? "" : "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
Analyze current project for additional context
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If checked, the agent will research your existing codebase to
|
||||
understand the tech stack. If unchecked, defaults to TanStack
|
||||
Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind CSS, and
|
||||
React.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 pt-2">
|
||||
<Checkbox
|
||||
id="create-generate-features"
|
||||
checked={generateFeatures}
|
||||
onCheckedChange={(checked) =>
|
||||
onGenerateFeaturesChange(checked === true)
|
||||
}
|
||||
disabled={isCreatingSpec}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="sidebar-generate-features"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
htmlFor="create-generate-features"
|
||||
className={`text-sm font-medium ${
|
||||
isCreatingSpec ? "" : "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
Generate feature list
|
||||
</label>
|
||||
@@ -124,7 +137,10 @@ export function ProjectSetupDialog({
|
||||
featureCount === option.value ? "default" : "outline"
|
||||
}
|
||||
size="sm"
|
||||
onClick={() => onFeatureCountChange(option.value)}
|
||||
onClick={() =>
|
||||
onFeatureCountChange(option.value as FeatureCount)
|
||||
}
|
||||
disabled={isCreatingSpec}
|
||||
className={cn(
|
||||
"flex-1 transition-all",
|
||||
featureCount === option.value
|
||||
@@ -137,14 +153,10 @@ export function ProjectSetupDialog({
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount)
|
||||
?.warning && (
|
||||
{selectedOption?.warning && (
|
||||
<p className="text-xs text-amber-500 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{
|
||||
FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount)
|
||||
?.warning
|
||||
}
|
||||
{selectedOption.warning}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -152,13 +164,37 @@ export function ProjectSetupDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={onSkip}>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button onClick={onCreateSpec} disabled={!projectOverview.trim()}>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Generate Spec
|
||||
</Button>
|
||||
{showSkipButton && onSkip ? (
|
||||
<Button variant="ghost" onClick={onSkip} disabled={isCreatingSpec}>
|
||||
Skip for now
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isCreatingSpec}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<HotkeyButton
|
||||
onClick={onCreateSpec}
|
||||
disabled={!projectOverview.trim() || isCreatingSpec}
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={open && !isCreatingSpec}
|
||||
>
|
||||
{isCreatingSpec ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Generate Spec
|
||||
</>
|
||||
)}
|
||||
</HotkeyButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
2
apps/ui/src/components/views/spec-view/dialogs/index.ts
Normal file
2
apps/ui/src/components/views/spec-view/dialogs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { CreateSpecDialog } from "./create-spec-dialog";
|
||||
export { RegenerateSpecDialog } from "./regenerate-spec-dialog";
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Sparkles, Clock, Loader2 } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FEATURE_COUNT_OPTIONS } from "../constants";
|
||||
import type { RegenerateSpecDialogProps, FeatureCount } from "../types";
|
||||
|
||||
export function RegenerateSpecDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
projectDefinition,
|
||||
onProjectDefinitionChange,
|
||||
generateFeatures,
|
||||
onGenerateFeaturesChange,
|
||||
analyzeProject,
|
||||
onAnalyzeProjectChange,
|
||||
featureCount,
|
||||
onFeatureCountChange,
|
||||
onRegenerate,
|
||||
isRegenerating,
|
||||
isGeneratingFeatures = false,
|
||||
}: RegenerateSpecDialogProps) {
|
||||
const selectedOption = FEATURE_COUNT_OPTIONS.find(
|
||||
(o) => o.value === featureCount
|
||||
);
|
||||
const isDisabled = isRegenerating || isGeneratingFeatures;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && !isRegenerating) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Regenerate App Specification</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
We will regenerate your app spec based on a short project definition
|
||||
and the current tech stack found in your project. The agent will
|
||||
analyze your codebase to understand your existing technologies and
|
||||
create a comprehensive specification.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Describe your project</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Provide a clear description of what your app should do. Be as
|
||||
detailed as you want - the more context you provide, the more
|
||||
comprehensive the spec will be.
|
||||
</p>
|
||||
<textarea
|
||||
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
value={projectDefinition}
|
||||
onChange={(e) => onProjectDefinitionChange(e.target.value)}
|
||||
placeholder="e.g., A task management app where users can create projects, add tasks with due dates, assign tasks to team members, track progress with a kanban board, and receive notifications for upcoming deadlines..."
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 pt-2">
|
||||
<Checkbox
|
||||
id="regenerate-analyze-project"
|
||||
checked={analyzeProject}
|
||||
onCheckedChange={(checked) =>
|
||||
onAnalyzeProjectChange(checked === true)
|
||||
}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="regenerate-analyze-project"
|
||||
className={`text-sm font-medium ${
|
||||
isDisabled ? "" : "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
Analyze current project for additional context
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If checked, the agent will research your existing codebase to
|
||||
understand the tech stack. If unchecked, defaults to TanStack
|
||||
Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind CSS, and
|
||||
React.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 pt-2">
|
||||
<Checkbox
|
||||
id="regenerate-generate-features"
|
||||
checked={generateFeatures}
|
||||
onCheckedChange={(checked) =>
|
||||
onGenerateFeaturesChange(checked === true)
|
||||
}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="regenerate-generate-features"
|
||||
className={`text-sm font-medium ${
|
||||
isDisabled ? "" : "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
Generate feature list
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Automatically create features in the features folder from the
|
||||
implementation roadmap after the spec is regenerated.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Count Selection - only shown when generateFeatures is enabled */}
|
||||
{generateFeatures && (
|
||||
<div className="space-y-2 pt-2 pl-7">
|
||||
<label className="text-sm font-medium">Number of Features</label>
|
||||
<div className="flex gap-2">
|
||||
{FEATURE_COUNT_OPTIONS.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
type="button"
|
||||
variant={
|
||||
featureCount === option.value ? "default" : "outline"
|
||||
}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
onFeatureCountChange(option.value as FeatureCount)
|
||||
}
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"flex-1 transition-all",
|
||||
featureCount === option.value
|
||||
? "bg-primary hover:bg-primary/90 text-primary-foreground"
|
||||
: "bg-muted/30 hover:bg-muted/50 border-border"
|
||||
)}
|
||||
data-testid={`regenerate-feature-count-${option.value}`}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{selectedOption?.warning && (
|
||||
<p className="text-xs text-amber-500 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{selectedOption.warning}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={onRegenerate}
|
||||
disabled={!projectDefinition.trim() || isDisabled}
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={open && !isDisabled}
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Regenerating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Regenerate Spec
|
||||
</>
|
||||
)}
|
||||
</HotkeyButton>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
3
apps/ui/src/components/views/spec-view/hooks/index.ts
Normal file
3
apps/ui/src/components/views/spec-view/hooks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { useSpecLoading } from "./use-spec-loading";
|
||||
export { useSpecSave } from "./use-spec-save";
|
||||
export { useSpecGeneration } from "./use-spec-generation";
|
||||
@@ -0,0 +1,662 @@
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { toast } from "sonner";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { createElement } from "react";
|
||||
import { SPEC_FILE_WRITE_DELAY, STATUS_CHECK_INTERVAL_MS } from "../constants";
|
||||
import type { FeatureCount } from "../types";
|
||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||
|
||||
interface UseSpecGenerationOptions {
|
||||
loadSpec: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||
const { currentProject } = useAppStore();
|
||||
|
||||
// Dialog visibility state
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
|
||||
|
||||
// Create spec state
|
||||
const [projectOverview, setProjectOverview] = useState("");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||
const [analyzeProjectOnCreate, setAnalyzeProjectOnCreate] = useState(true);
|
||||
const [featureCountOnCreate, setFeatureCountOnCreate] =
|
||||
useState<FeatureCount>(50);
|
||||
|
||||
// Regenerate spec state
|
||||
const [projectDefinition, setProjectDefinition] = useState("");
|
||||
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||
const [generateFeaturesOnRegenerate, setGenerateFeaturesOnRegenerate] =
|
||||
useState(true);
|
||||
const [analyzeProjectOnRegenerate, setAnalyzeProjectOnRegenerate] =
|
||||
useState(true);
|
||||
const [featureCountOnRegenerate, setFeatureCountOnRegenerate] =
|
||||
useState<FeatureCount>(50);
|
||||
|
||||
// Generate features only state
|
||||
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
|
||||
|
||||
// Logs state (kept for internal tracking)
|
||||
const [logs, setLogs] = useState<string>("");
|
||||
const logsRef = useRef<string>("");
|
||||
|
||||
// Phase tracking and status
|
||||
const [currentPhase, setCurrentPhase] = useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const statusCheckRef = useRef<boolean>(false);
|
||||
const stateRestoredRef = useRef<boolean>(false);
|
||||
const pendingStatusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Reset all state when project changes
|
||||
useEffect(() => {
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("");
|
||||
setErrorMessage("");
|
||||
setLogs("");
|
||||
logsRef.current = "";
|
||||
stateRestoredRef.current = false;
|
||||
statusCheckRef.current = false;
|
||||
|
||||
if (pendingStatusTimeoutRef.current) {
|
||||
clearTimeout(pendingStatusTimeoutRef.current);
|
||||
pendingStatusTimeoutRef.current = null;
|
||||
}
|
||||
}, [currentProject?.path]);
|
||||
|
||||
// Check if spec regeneration is running when component mounts or project changes
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
if (!currentProject || statusCheckRef.current) return;
|
||||
statusCheckRef.current = true;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
statusCheckRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await api.specRegeneration.status();
|
||||
console.log(
|
||||
"[useSpecGeneration] Status check on mount:",
|
||||
status,
|
||||
"for project:",
|
||||
currentProject.path
|
||||
);
|
||||
|
||||
if (status.success && status.isRunning) {
|
||||
console.log(
|
||||
"[useSpecGeneration] Spec generation is running globally. Tentatively showing loader."
|
||||
);
|
||||
|
||||
setIsCreating(true);
|
||||
setIsRegenerating(true);
|
||||
if (status.currentPhase) {
|
||||
setCurrentPhase(status.currentPhase);
|
||||
} else {
|
||||
setCurrentPhase("initialization");
|
||||
}
|
||||
|
||||
if (pendingStatusTimeoutRef.current) {
|
||||
clearTimeout(pendingStatusTimeoutRef.current);
|
||||
}
|
||||
pendingStatusTimeoutRef.current = setTimeout(() => {
|
||||
console.log(
|
||||
"[useSpecGeneration] No events received for current project - clearing tentative state"
|
||||
);
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setCurrentPhase("");
|
||||
pendingStatusTimeoutRef.current = null;
|
||||
}, 3000);
|
||||
} else if (status.success && !status.isRunning) {
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setCurrentPhase("");
|
||||
stateRestoredRef.current = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[useSpecGeneration] Failed to check status:", error);
|
||||
} finally {
|
||||
statusCheckRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
stateRestoredRef.current = false;
|
||||
checkStatus();
|
||||
}, [currentProject]);
|
||||
|
||||
// Sync state when tab becomes visible
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = async () => {
|
||||
if (
|
||||
!document.hidden &&
|
||||
currentProject &&
|
||||
(isCreating || isRegenerating || isGeneratingFeatures)
|
||||
) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) return;
|
||||
|
||||
const status = await api.specRegeneration.status();
|
||||
console.log(
|
||||
"[useSpecGeneration] Visibility change - status check:",
|
||||
status
|
||||
);
|
||||
|
||||
if (!status.isRunning) {
|
||||
console.log(
|
||||
"[useSpecGeneration] Visibility change: Backend indicates generation complete - clearing state"
|
||||
);
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("");
|
||||
stateRestoredRef.current = false;
|
||||
loadSpec();
|
||||
} else if (status.currentPhase) {
|
||||
setCurrentPhase(status.currentPhase);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useSpecGeneration] Failed to check status on visibility change:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [
|
||||
currentProject,
|
||||
isCreating,
|
||||
isRegenerating,
|
||||
isGeneratingFeatures,
|
||||
loadSpec,
|
||||
]);
|
||||
|
||||
// Periodic status check
|
||||
useEffect(() => {
|
||||
if (
|
||||
!currentProject ||
|
||||
(!isCreating && !isRegenerating && !isGeneratingFeatures)
|
||||
)
|
||||
return;
|
||||
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) return;
|
||||
|
||||
const status = await api.specRegeneration.status();
|
||||
|
||||
if (!status.isRunning) {
|
||||
console.log(
|
||||
"[useSpecGeneration] Periodic check: Backend indicates generation complete - clearing state"
|
||||
);
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("");
|
||||
stateRestoredRef.current = false;
|
||||
loadSpec();
|
||||
} else if (
|
||||
status.currentPhase &&
|
||||
status.currentPhase !== currentPhase
|
||||
) {
|
||||
console.log(
|
||||
"[useSpecGeneration] Periodic check: Phase updated from backend",
|
||||
{
|
||||
old: currentPhase,
|
||||
new: status.currentPhase,
|
||||
}
|
||||
);
|
||||
setCurrentPhase(status.currentPhase);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useSpecGeneration] Periodic status check error:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}, STATUS_CHECK_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [
|
||||
currentProject,
|
||||
isCreating,
|
||||
isRegenerating,
|
||||
isGeneratingFeatures,
|
||||
currentPhase,
|
||||
loadSpec,
|
||||
]);
|
||||
|
||||
// Subscribe to spec regeneration events
|
||||
useEffect(() => {
|
||||
if (!currentProject) return;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) return;
|
||||
|
||||
const unsubscribe = api.specRegeneration.onEvent(
|
||||
(event: SpecRegenerationEvent) => {
|
||||
console.log(
|
||||
"[useSpecGeneration] Regeneration event:",
|
||||
event.type,
|
||||
"for project:",
|
||||
event.projectPath,
|
||||
"current project:",
|
||||
currentProject?.path
|
||||
);
|
||||
|
||||
if (event.projectPath !== currentProject?.path) {
|
||||
console.log(
|
||||
"[useSpecGeneration] Ignoring event - not for current project"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingStatusTimeoutRef.current) {
|
||||
clearTimeout(pendingStatusTimeoutRef.current);
|
||||
pendingStatusTimeoutRef.current = null;
|
||||
console.log(
|
||||
"[useSpecGeneration] Event confirmed this is for current project - clearing timeout"
|
||||
);
|
||||
}
|
||||
|
||||
if (event.type === "spec_regeneration_progress") {
|
||||
setIsCreating(true);
|
||||
setIsRegenerating(true);
|
||||
|
||||
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
|
||||
if (phaseMatch) {
|
||||
const phase = phaseMatch[1];
|
||||
setCurrentPhase(phase);
|
||||
console.log(`[useSpecGeneration] Phase updated: ${phase}`);
|
||||
|
||||
if (phase === "complete") {
|
||||
console.log(
|
||||
"[useSpecGeneration] Phase is complete - clearing state"
|
||||
);
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
stateRestoredRef.current = false;
|
||||
setTimeout(() => {
|
||||
loadSpec();
|
||||
}, SPEC_FILE_WRITE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.content.includes("All tasks completed") ||
|
||||
event.content.includes("✓ All tasks completed")
|
||||
) {
|
||||
console.log(
|
||||
"[useSpecGeneration] Detected completion in progress message - clearing state"
|
||||
);
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setCurrentPhase("");
|
||||
stateRestoredRef.current = false;
|
||||
setTimeout(() => {
|
||||
loadSpec();
|
||||
}, SPEC_FILE_WRITE_DELAY);
|
||||
}
|
||||
|
||||
const newLog = logsRef.current + event.content;
|
||||
logsRef.current = newLog;
|
||||
setLogs(newLog);
|
||||
console.log(
|
||||
"[useSpecGeneration] Progress:",
|
||||
event.content.substring(0, 100)
|
||||
);
|
||||
|
||||
if (errorMessage) {
|
||||
setErrorMessage("");
|
||||
}
|
||||
} else if (event.type === "spec_regeneration_tool") {
|
||||
const isFeatureTool =
|
||||
event.tool === "mcp__automaker-tools__UpdateFeatureStatus" ||
|
||||
event.tool === "UpdateFeatureStatus" ||
|
||||
event.tool?.includes("Feature");
|
||||
|
||||
if (isFeatureTool) {
|
||||
if (currentPhase !== "feature_generation") {
|
||||
setCurrentPhase("feature_generation");
|
||||
setIsCreating(true);
|
||||
setIsRegenerating(true);
|
||||
console.log(
|
||||
"[useSpecGeneration] Detected feature creation tool - setting phase to feature_generation"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const toolInput = event.input
|
||||
? ` (${JSON.stringify(event.input).substring(0, 100)}...)`
|
||||
: "";
|
||||
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
|
||||
const newLog = logsRef.current + toolLog;
|
||||
logsRef.current = newLog;
|
||||
setLogs(newLog);
|
||||
console.log("[useSpecGeneration] Tool:", event.tool, event.input);
|
||||
} else if (event.type === "spec_regeneration_complete") {
|
||||
const completionLog =
|
||||
logsRef.current + `\n[Complete] ${event.message}\n`;
|
||||
logsRef.current = completionLog;
|
||||
setLogs(completionLog);
|
||||
|
||||
const isFinalCompletionMessage =
|
||||
event.message?.includes("All tasks completed") ||
|
||||
event.message === "All tasks completed!" ||
|
||||
event.message === "All tasks completed" ||
|
||||
event.message === "Spec regeneration complete!" ||
|
||||
event.message === "Initial spec creation complete!";
|
||||
|
||||
const hasCompletePhase =
|
||||
logsRef.current.includes("[Phase: complete]");
|
||||
|
||||
const isIntermediateCompletion =
|
||||
event.message?.includes("Features are being generated") ||
|
||||
event.message?.includes("features are being generated");
|
||||
|
||||
const shouldComplete =
|
||||
(isFinalCompletionMessage || hasCompletePhase) &&
|
||||
!isIntermediateCompletion;
|
||||
|
||||
if (shouldComplete) {
|
||||
console.log(
|
||||
"[useSpecGeneration] Final completion detected - clearing state",
|
||||
{
|
||||
isFinalCompletionMessage,
|
||||
hasCompletePhase,
|
||||
message: event.message,
|
||||
}
|
||||
);
|
||||
setIsRegenerating(false);
|
||||
setIsCreating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("");
|
||||
setShowRegenerateDialog(false);
|
||||
setShowCreateDialog(false);
|
||||
setProjectDefinition("");
|
||||
setProjectOverview("");
|
||||
setErrorMessage("");
|
||||
stateRestoredRef.current = false;
|
||||
|
||||
setTimeout(() => {
|
||||
loadSpec();
|
||||
}, SPEC_FILE_WRITE_DELAY);
|
||||
|
||||
const isRegeneration = event.message?.includes("regeneration");
|
||||
const isFeatureGeneration =
|
||||
event.message?.includes("Feature generation");
|
||||
toast.success(
|
||||
isFeatureGeneration
|
||||
? "Feature Generation Complete"
|
||||
: isRegeneration
|
||||
? "Spec Regeneration Complete"
|
||||
: "Spec Creation Complete",
|
||||
{
|
||||
description: isFeatureGeneration
|
||||
? "Features have been created from the app specification."
|
||||
: "Your app specification has been saved.",
|
||||
icon: createElement(CheckCircle2, { className: "w-4 h-4" }),
|
||||
}
|
||||
);
|
||||
} else if (isIntermediateCompletion) {
|
||||
setIsCreating(true);
|
||||
setIsRegenerating(true);
|
||||
setCurrentPhase("feature_generation");
|
||||
console.log(
|
||||
"[useSpecGeneration] Intermediate completion, continuing with feature generation"
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[useSpecGeneration] Spec generation event:",
|
||||
event.message
|
||||
);
|
||||
} else if (event.type === "spec_regeneration_error") {
|
||||
setIsRegenerating(false);
|
||||
setIsCreating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("error");
|
||||
setErrorMessage(event.error);
|
||||
stateRestoredRef.current = false;
|
||||
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
|
||||
logsRef.current = errorLog;
|
||||
setLogs(errorLog);
|
||||
console.error("[useSpecGeneration] Regeneration error:", event.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [currentProject?.path, loadSpec, errorMessage, currentPhase]);
|
||||
|
||||
// Handler functions
|
||||
const handleCreateSpec = useCallback(async () => {
|
||||
if (!currentProject || !projectOverview.trim()) return;
|
||||
|
||||
setIsCreating(true);
|
||||
setShowCreateDialog(false);
|
||||
setCurrentPhase("initialization");
|
||||
setErrorMessage("");
|
||||
logsRef.current = "";
|
||||
setLogs("");
|
||||
console.log(
|
||||
"[useSpecGeneration] Starting spec creation, generateFeatures:",
|
||||
generateFeatures
|
||||
);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
console.error("[useSpecGeneration] Spec regeneration not available");
|
||||
setIsCreating(false);
|
||||
return;
|
||||
}
|
||||
const result = await api.specRegeneration.create(
|
||||
currentProject.path,
|
||||
projectOverview.trim(),
|
||||
generateFeatures,
|
||||
analyzeProjectOnCreate,
|
||||
generateFeatures ? featureCountOnCreate : undefined
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error || "Unknown error";
|
||||
console.error(
|
||||
"[useSpecGeneration] Failed to start spec creation:",
|
||||
errorMsg
|
||||
);
|
||||
setIsCreating(false);
|
||||
setCurrentPhase("error");
|
||||
setErrorMessage(errorMsg);
|
||||
const errorLog = `[Error] Failed to start spec creation: ${errorMsg}\n`;
|
||||
logsRef.current = errorLog;
|
||||
setLogs(errorLog);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error("[useSpecGeneration] Failed to create spec:", errorMsg);
|
||||
setIsCreating(false);
|
||||
setCurrentPhase("error");
|
||||
setErrorMessage(errorMsg);
|
||||
const errorLog = `[Error] Failed to create spec: ${errorMsg}\n`;
|
||||
logsRef.current = errorLog;
|
||||
setLogs(errorLog);
|
||||
}
|
||||
}, [
|
||||
currentProject,
|
||||
projectOverview,
|
||||
generateFeatures,
|
||||
analyzeProjectOnCreate,
|
||||
featureCountOnCreate,
|
||||
]);
|
||||
|
||||
const handleRegenerate = useCallback(async () => {
|
||||
if (!currentProject || !projectDefinition.trim()) return;
|
||||
|
||||
setIsRegenerating(true);
|
||||
setShowRegenerateDialog(false);
|
||||
setCurrentPhase("initialization");
|
||||
setErrorMessage("");
|
||||
logsRef.current = "";
|
||||
setLogs("");
|
||||
console.log(
|
||||
"[useSpecGeneration] Starting spec regeneration, generateFeatures:",
|
||||
generateFeaturesOnRegenerate
|
||||
);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
console.error("[useSpecGeneration] Spec regeneration not available");
|
||||
setIsRegenerating(false);
|
||||
return;
|
||||
}
|
||||
const result = await api.specRegeneration.generate(
|
||||
currentProject.path,
|
||||
projectDefinition.trim(),
|
||||
generateFeaturesOnRegenerate,
|
||||
analyzeProjectOnRegenerate,
|
||||
generateFeaturesOnRegenerate ? featureCountOnRegenerate : undefined
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error || "Unknown error";
|
||||
console.error(
|
||||
"[useSpecGeneration] Failed to start regeneration:",
|
||||
errorMsg
|
||||
);
|
||||
setIsRegenerating(false);
|
||||
setCurrentPhase("error");
|
||||
setErrorMessage(errorMsg);
|
||||
const errorLog = `[Error] Failed to start regeneration: ${errorMsg}\n`;
|
||||
logsRef.current = errorLog;
|
||||
setLogs(errorLog);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error("[useSpecGeneration] Failed to regenerate spec:", errorMsg);
|
||||
setIsRegenerating(false);
|
||||
setCurrentPhase("error");
|
||||
setErrorMessage(errorMsg);
|
||||
const errorLog = `[Error] Failed to regenerate spec: ${errorMsg}\n`;
|
||||
logsRef.current = errorLog;
|
||||
setLogs(errorLog);
|
||||
}
|
||||
}, [
|
||||
currentProject,
|
||||
projectDefinition,
|
||||
generateFeaturesOnRegenerate,
|
||||
analyzeProjectOnRegenerate,
|
||||
featureCountOnRegenerate,
|
||||
]);
|
||||
|
||||
const handleGenerateFeatures = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsGeneratingFeatures(true);
|
||||
setShowRegenerateDialog(false);
|
||||
setCurrentPhase("initialization");
|
||||
setErrorMessage("");
|
||||
logsRef.current = "";
|
||||
setLogs("");
|
||||
console.log(
|
||||
"[useSpecGeneration] Starting feature generation from existing spec"
|
||||
);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
console.error("[useSpecGeneration] Spec regeneration not available");
|
||||
setIsGeneratingFeatures(false);
|
||||
return;
|
||||
}
|
||||
const result = await api.specRegeneration.generateFeatures(
|
||||
currentProject.path
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error || "Unknown error";
|
||||
console.error(
|
||||
"[useSpecGeneration] Failed to start feature generation:",
|
||||
errorMsg
|
||||
);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("error");
|
||||
setErrorMessage(errorMsg);
|
||||
const errorLog = `[Error] Failed to start feature generation: ${errorMsg}\n`;
|
||||
logsRef.current = errorLog;
|
||||
setLogs(errorLog);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
"[useSpecGeneration] Failed to generate features:",
|
||||
errorMsg
|
||||
);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("error");
|
||||
setErrorMessage(errorMsg);
|
||||
const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`;
|
||||
logsRef.current = errorLog;
|
||||
setLogs(errorLog);
|
||||
}
|
||||
}, [currentProject]);
|
||||
|
||||
return {
|
||||
// Dialog state
|
||||
showCreateDialog,
|
||||
setShowCreateDialog,
|
||||
showRegenerateDialog,
|
||||
setShowRegenerateDialog,
|
||||
|
||||
// Create state
|
||||
projectOverview,
|
||||
setProjectOverview,
|
||||
isCreating,
|
||||
generateFeatures,
|
||||
setGenerateFeatures,
|
||||
analyzeProjectOnCreate,
|
||||
setAnalyzeProjectOnCreate,
|
||||
featureCountOnCreate,
|
||||
setFeatureCountOnCreate,
|
||||
|
||||
// Regenerate state
|
||||
projectDefinition,
|
||||
setProjectDefinition,
|
||||
isRegenerating,
|
||||
generateFeaturesOnRegenerate,
|
||||
setGenerateFeaturesOnRegenerate,
|
||||
analyzeProjectOnRegenerate,
|
||||
setAnalyzeProjectOnRegenerate,
|
||||
featureCountOnRegenerate,
|
||||
setFeatureCountOnRegenerate,
|
||||
|
||||
// Feature generation state
|
||||
isGeneratingFeatures,
|
||||
|
||||
// Status state
|
||||
currentPhase,
|
||||
errorMessage,
|
||||
logs,
|
||||
|
||||
// Handlers
|
||||
handleCreateSpec,
|
||||
handleRegenerate,
|
||||
handleGenerateFeatures,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
|
||||
export function useSpecLoading() {
|
||||
const { currentProject, setAppSpec } = useAppStore();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [specExists, setSpecExists] = useState(true);
|
||||
|
||||
const loadSpec = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readFile(
|
||||
`${currentProject.path}/.automaker/app_spec.txt`
|
||||
);
|
||||
|
||||
if (result.success && result.content) {
|
||||
setAppSpec(result.content);
|
||||
setSpecExists(true);
|
||||
} else {
|
||||
// File doesn't exist
|
||||
setAppSpec("");
|
||||
setSpecExists(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load spec:", error);
|
||||
setSpecExists(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentProject, setAppSpec]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSpec();
|
||||
}, [loadSpec]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
specExists,
|
||||
setSpecExists,
|
||||
loadSpec,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useState } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
|
||||
export function useSpecSave() {
|
||||
const { currentProject, appSpec, setAppSpec } = useAppStore();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const saveSpec = async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
await api.writeFile(
|
||||
`${currentProject.path}/.automaker/app_spec.txt`,
|
||||
appSpec
|
||||
);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to save spec:", error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setAppSpec(value);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
hasChanges,
|
||||
setHasChanges,
|
||||
saveSpec,
|
||||
handleChange,
|
||||
};
|
||||
}
|
||||
50
apps/ui/src/components/views/spec-view/types.ts
Normal file
50
apps/ui/src/components/views/spec-view/types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Feature count options for spec generation
|
||||
export type FeatureCount = 20 | 50 | 100;
|
||||
|
||||
// Generation phases for UI display
|
||||
export type GenerationPhase =
|
||||
| "initialization"
|
||||
| "setup"
|
||||
| "analysis"
|
||||
| "spec_complete"
|
||||
| "feature_generation"
|
||||
| "complete"
|
||||
| "error";
|
||||
|
||||
// Props for the unified create spec dialog
|
||||
export interface CreateSpecDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
projectOverview: string;
|
||||
onProjectOverviewChange: (value: string) => void;
|
||||
generateFeatures: boolean;
|
||||
onGenerateFeaturesChange: (value: boolean) => void;
|
||||
analyzeProject: boolean;
|
||||
onAnalyzeProjectChange: (value: boolean) => void;
|
||||
featureCount: FeatureCount;
|
||||
onFeatureCountChange: (value: FeatureCount) => void;
|
||||
onCreateSpec: () => void;
|
||||
onSkip?: () => void;
|
||||
isCreatingSpec: boolean;
|
||||
showSkipButton?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Props for the regenerate spec dialog
|
||||
export interface RegenerateSpecDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
projectDefinition: string;
|
||||
onProjectDefinitionChange: (value: string) => void;
|
||||
generateFeatures: boolean;
|
||||
onGenerateFeaturesChange: (value: boolean) => void;
|
||||
analyzeProject: boolean;
|
||||
onAnalyzeProjectChange: (value: boolean) => void;
|
||||
featureCount: FeatureCount;
|
||||
onFeatureCountChange: (value: FeatureCount) => void;
|
||||
onRegenerate: () => void;
|
||||
onGenerateFeaturesOnly?: () => void;
|
||||
isRegenerating: boolean;
|
||||
isGeneratingFeatures?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user