mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +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 type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
||||||
import { NewProjectModal } from "@/components/new-project-modal";
|
import { NewProjectModal } from "@/components/new-project-modal";
|
||||||
import {
|
import { CreateSpecDialog } from "@/components/views/spec-view/dialogs";
|
||||||
ProjectSetupDialog,
|
import type { FeatureCount } from "@/components/views/spec-view/types";
|
||||||
type FeatureCount,
|
|
||||||
} from "@/components/layout/project-setup-dialog";
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -290,6 +288,7 @@ export function Sidebar() {
|
|||||||
const [setupProjectPath, setSetupProjectPath] = useState("");
|
const [setupProjectPath, setSetupProjectPath] = useState("");
|
||||||
const [projectOverview, setProjectOverview] = useState("");
|
const [projectOverview, setProjectOverview] = useState("");
|
||||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||||
|
const [analyzeProject, setAnalyzeProject] = useState(true);
|
||||||
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
|
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
|
||||||
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
||||||
|
|
||||||
@@ -496,7 +495,7 @@ export function Sidebar() {
|
|||||||
setupProjectPath,
|
setupProjectPath,
|
||||||
projectOverview.trim(),
|
projectOverview.trim(),
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
undefined, // analyzeProject - use default
|
analyzeProject,
|
||||||
generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features
|
generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -525,6 +524,7 @@ export function Sidebar() {
|
|||||||
setupProjectPath,
|
setupProjectPath,
|
||||||
projectOverview,
|
projectOverview,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
|
analyzeProject,
|
||||||
featureCount,
|
featureCount,
|
||||||
setSpecCreatingForProject,
|
setSpecCreatingForProject,
|
||||||
]);
|
]);
|
||||||
@@ -2276,18 +2276,23 @@ export function Sidebar() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* New Project Setup Dialog */}
|
{/* New Project Setup Dialog */}
|
||||||
<ProjectSetupDialog
|
<CreateSpecDialog
|
||||||
open={showSetupDialog}
|
open={showSetupDialog}
|
||||||
onOpenChange={setShowSetupDialog}
|
onOpenChange={setShowSetupDialog}
|
||||||
projectOverview={projectOverview}
|
projectOverview={projectOverview}
|
||||||
onProjectOverviewChange={setProjectOverview}
|
onProjectOverviewChange={setProjectOverview}
|
||||||
generateFeatures={generateFeatures}
|
generateFeatures={generateFeatures}
|
||||||
onGenerateFeaturesChange={setGenerateFeatures}
|
onGenerateFeaturesChange={setGenerateFeatures}
|
||||||
|
analyzeProject={analyzeProject}
|
||||||
|
onAnalyzeProjectChange={setAnalyzeProject}
|
||||||
featureCount={featureCount}
|
featureCount={featureCount}
|
||||||
onFeatureCountChange={setFeatureCount}
|
onFeatureCountChange={setFeatureCount}
|
||||||
onCreateSpec={handleCreateInitialSpec}
|
onCreateSpec={handleCreateInitialSpec}
|
||||||
onSkip={handleSkipSetup}
|
onSkip={handleSkipSetup}
|
||||||
isCreatingSpec={isCreatingSpec}
|
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 */}
|
{/* 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, Loader2 } from "lucide-react";
|
||||||
import { Sparkles, Clock } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -9,66 +8,49 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FEATURE_COUNT_OPTIONS } from "../constants";
|
||||||
|
import type { CreateSpecDialogProps, FeatureCount } from "../types";
|
||||||
|
|
||||||
// Feature count options
|
export function CreateSpecDialog({
|
||||||
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({
|
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
projectOverview,
|
projectOverview,
|
||||||
onProjectOverviewChange,
|
onProjectOverviewChange,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
onGenerateFeaturesChange,
|
onGenerateFeaturesChange,
|
||||||
|
analyzeProject,
|
||||||
|
onAnalyzeProjectChange,
|
||||||
featureCount,
|
featureCount,
|
||||||
onFeatureCountChange,
|
onFeatureCountChange,
|
||||||
onCreateSpec,
|
onCreateSpec,
|
||||||
onSkip,
|
onSkip,
|
||||||
isCreatingSpec,
|
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 (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
onOpenChange(open);
|
onOpenChange(open);
|
||||||
if (!open && !isCreatingSpec) {
|
if (!open && !isCreatingSpec && onSkip) {
|
||||||
onSkip();
|
onSkip();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Set Up Your Project</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogDescription className="text-muted-foreground">
|
||||||
We didn't find an app_spec.txt file. Let us help you generate
|
{description}
|
||||||
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.
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -86,21 +68,52 @@ export function ProjectSetupDialog({
|
|||||||
onChange={(e) => onProjectOverviewChange(e.target.value)}
|
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..."
|
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
|
autoFocus
|
||||||
|
disabled={isCreatingSpec}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start space-x-3 pt-2">
|
<div className="flex items-start space-x-3 pt-2">
|
||||||
<Checkbox
|
<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}
|
checked={generateFeatures}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
onGenerateFeaturesChange(checked === true)
|
onGenerateFeaturesChange(checked === true)
|
||||||
}
|
}
|
||||||
|
disabled={isCreatingSpec}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label
|
<label
|
||||||
htmlFor="sidebar-generate-features"
|
htmlFor="create-generate-features"
|
||||||
className="text-sm font-medium cursor-pointer"
|
className={`text-sm font-medium ${
|
||||||
|
isCreatingSpec ? "" : "cursor-pointer"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
Generate feature list
|
Generate feature list
|
||||||
</label>
|
</label>
|
||||||
@@ -124,7 +137,10 @@ export function ProjectSetupDialog({
|
|||||||
featureCount === option.value ? "default" : "outline"
|
featureCount === option.value ? "default" : "outline"
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onFeatureCountChange(option.value)}
|
onClick={() =>
|
||||||
|
onFeatureCountChange(option.value as FeatureCount)
|
||||||
|
}
|
||||||
|
disabled={isCreatingSpec}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 transition-all",
|
"flex-1 transition-all",
|
||||||
featureCount === option.value
|
featureCount === option.value
|
||||||
@@ -137,14 +153,10 @@ export function ProjectSetupDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount)
|
{selectedOption?.warning && (
|
||||||
?.warning && (
|
|
||||||
<p className="text-xs text-amber-500 flex items-center gap-1">
|
<p className="text-xs text-amber-500 flex items-center gap-1">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
{
|
{selectedOption.warning}
|
||||||
FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount)
|
|
||||||
?.warning
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -152,13 +164,37 @@ export function ProjectSetupDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={onSkip}>
|
{showSkipButton && onSkip ? (
|
||||||
Skip for now
|
<Button variant="ghost" onClick={onSkip} disabled={isCreatingSpec}>
|
||||||
</Button>
|
Skip for now
|
||||||
<Button onClick={onCreateSpec} disabled={!projectOverview.trim()}>
|
</Button>
|
||||||
<Sparkles className="w-4 h-4 mr-2" />
|
) : (
|
||||||
Generate Spec
|
<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>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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