@theproductguy/create-mission-control 1.0.17 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/template/agent-os/WORKFLOW.md +139 -0
- package/src/template/agent-os/commands/adapt/adapt.md +189 -0
- package/src/template/agent-os/commands/animate/animate.md +184 -0
- package/src/template/agent-os/commands/audit/audit.md +123 -0
- package/src/template/agent-os/commands/bolder/bolder.md +126 -0
- package/src/template/agent-os/commands/clarify/clarify.md +173 -0
- package/src/template/agent-os/commands/colorize/colorize.md +152 -0
- package/src/template/agent-os/commands/critique/critique.md +112 -0
- package/src/template/agent-os/commands/delight/delight.md +311 -0
- package/src/template/agent-os/commands/design-screen/design-screen.md +5 -0
- package/src/template/agent-os/commands/design-shell/design-shell.md +5 -0
- package/src/template/agent-os/commands/design-tokens/design-tokens.md +5 -0
- package/src/template/agent-os/commands/extract/extract.md +88 -0
- package/src/template/agent-os/commands/harden/harden.md +351 -0
- package/src/template/agent-os/commands/impeccable/impeccable.md +163 -0
- package/src/template/agent-os/commands/normalize/normalize.md +61 -0
- package/src/template/agent-os/commands/onboard/onboard.md +236 -0
- package/src/template/agent-os/commands/optimize/optimize.md +262 -0
- package/src/template/agent-os/commands/plan-product/3-create-roadmap.md +7 -3
- package/src/template/agent-os/commands/polish/polish.md +196 -0
- package/src/template/agent-os/commands/quieter/quieter.md +112 -0
- package/src/template/agent-os/commands/simplify/simplify.md +131 -0
- package/src/template/agent-os/commands/teach-impeccable/teach-impeccable.md +67 -0
- package/src/template/control-center/backend/index.js +1 -1
- package/src/template/control-center/frontend/src/App.tsx +85 -839
- package/src/template/control-center/frontend/src/components/DesignOSOverlay.tsx +38 -0
- package/src/template/control-center/frontend/src/components/Guidance.tsx +56 -0
- package/src/template/control-center/frontend/src/components/NextStepCard.tsx +115 -0
- package/src/template/control-center/frontend/src/components/PromptButton.tsx +43 -0
- package/src/template/control-center/frontend/src/components/StatusItem.tsx +38 -0
- package/src/template/control-center/frontend/src/components/modals/CreateSpecModal.tsx +73 -0
- package/src/template/control-center/frontend/src/components/modals/DeleteSpecModal.tsx +42 -0
- package/src/template/control-center/frontend/src/components/modals/FileEditorModal.tsx +87 -0
- package/src/template/control-center/frontend/src/components/modals/SettingsModal.tsx +46 -0
- package/src/template/control-center/frontend/src/components/ui/ToastContext.tsx +1 -1
- package/src/template/control-center/frontend/src/contexts/IdeContext.tsx +6 -0
- package/src/template/control-center/frontend/src/hooks/useFileEditor.ts +42 -0
- package/src/template/control-center/frontend/src/hooks/useProjectState.ts +45 -0
- package/src/template/control-center/frontend/src/index.css +26 -0
- package/src/template/control-center/frontend/src/types.ts +65 -0
- package/src/template/control-center/frontend/tailwind.config.js +15 -3
- package/src/template/control-center/product/design-system/QA/audit-report.md +34 -0
- package/src/template/control-center/product/mission.md +38 -0
- package/src/template/control-center/product/roadmap.md +10 -0
- package/src/template/design-system/src/lib/product-loader.ts +6 -0
- package/src/template/package-lock.json +2756 -134
- package/src/template/package.json +2 -2
- package/src/template/agent-os-ui/_package.json +0 -54
- package/src/template/agent-os-ui/package.json +0 -54
- package/src/template/control-center/frontend/src/components/ThemeToggle.tsx +0 -64
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Layers, X } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
interface DesignOSOverlayProps {
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const DesignOSOverlay = ({ onClose }: DesignOSOverlayProps) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className="fixed inset-0 z-[100] bg-background flex flex-col animate-fade-in w-screen h-screen">
|
|
10
|
+
<div className="h-14 border-b border-border flex items-center justify-between px-6 bg-card shrink-0 shadow-sm z-50">
|
|
11
|
+
<div className="flex items-center gap-3">
|
|
12
|
+
<div className="p-1.5 bg-sidebar-primary/10 text-sidebar-primary rounded-md">
|
|
13
|
+
<Layers size={20} />
|
|
14
|
+
</div>
|
|
15
|
+
<h2 className="font-semibold text-lg text-foreground">Design OS</h2>
|
|
16
|
+
<span className="text-muted-foreground text-sm border-l border-border pl-3">Design System & Handoff</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div className="flex items-center gap-3">
|
|
19
|
+
<button
|
|
20
|
+
onClick={onClose}
|
|
21
|
+
className="p-2 text-muted-foreground hover:text-foreground hover:bg-secondary rounded-md transition-colors"
|
|
22
|
+
title="Close Design OS"
|
|
23
|
+
>
|
|
24
|
+
<X size={20} />
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="flex-1 bg-background relative">
|
|
29
|
+
<iframe
|
|
30
|
+
src={`http://localhost:5400?theme=${localStorage.getItem('theme') || 'system'}`}
|
|
31
|
+
className="absolute inset-0 w-full h-full border-none"
|
|
32
|
+
title="Design OS"
|
|
33
|
+
allow="clipboard-read; clipboard-write text/html" // updated per chrome warnings usually
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
</div >
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ArrowRight, Copy } from 'lucide-react';
|
|
2
|
+
import { useToast } from '../components/ui/ToastContext';
|
|
3
|
+
|
|
4
|
+
interface GuidanceProps {
|
|
5
|
+
phase: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
prompt?: string;
|
|
9
|
+
actionLabel?: string;
|
|
10
|
+
onAction?: () => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Guidance = ({ phase, title, description, prompt, actionLabel, onAction, className }: GuidanceProps) => {
|
|
15
|
+
const { toast } = useToast();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={`bg-card border border-border rounded-xl p-6 shadow-sm ${className}`}>
|
|
19
|
+
<div className="text-xs font-semibold tracking-wider text-muted-foreground uppercase mb-2">
|
|
20
|
+
{phase}
|
|
21
|
+
</div>
|
|
22
|
+
<div className="flex flex-col md:flex-row gap-6 items-start justify-between">
|
|
23
|
+
<div className="max-w-xl">
|
|
24
|
+
<h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
|
|
25
|
+
<p className="text-muted-foreground leading-relaxed">
|
|
26
|
+
{description}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
{(prompt || actionLabel) && (
|
|
30
|
+
<div className="flex items-center gap-3 shrink-0">
|
|
31
|
+
{prompt && (
|
|
32
|
+
<button
|
|
33
|
+
onClick={() => {
|
|
34
|
+
navigator.clipboard.writeText(prompt);
|
|
35
|
+
toast({ title: "Prompt Copied!", type: 'success' });
|
|
36
|
+
}}
|
|
37
|
+
className="flex items-center gap-2 px-4 py-2.5 bg-secondary hover:bg-secondary/80 text-secondary-foreground font-medium rounded-lg border border-border transition-all shadow-sm hover:shadow"
|
|
38
|
+
>
|
|
39
|
+
<Copy size={16} />
|
|
40
|
+
Copy Prompt
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
{actionLabel && (
|
|
44
|
+
<button
|
|
45
|
+
onClick={onAction}
|
|
46
|
+
className="flex items-center gap-2 px-4 py-2.5 bg-primary hover:bg-primary/90 text-primary-foreground font-medium rounded-lg shadow-sm hover:shadow-md transition-all"
|
|
47
|
+
>
|
|
48
|
+
{actionLabel} <ArrowRight size={16} />
|
|
49
|
+
</button>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ProjectState } from '../types';
|
|
2
|
+
import { Guidance } from './Guidance';
|
|
3
|
+
|
|
4
|
+
interface NextStepCardProps {
|
|
5
|
+
state: ProjectState;
|
|
6
|
+
onOpenDesign: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const NextStepCard = ({ state, onOpenDesign }: NextStepCardProps) => {
|
|
10
|
+
let step = { phase: "", title: "", description: "", prompt: "", link: "", actionLabel: "", action: () => { } };
|
|
11
|
+
|
|
12
|
+
const isProductComplete =
|
|
13
|
+
state?.product?.mission?.exists && !state?.product?.mission?.isBoilerplate &&
|
|
14
|
+
state?.product?.techStack?.exists && !state?.product?.techStack?.isBoilerplate &&
|
|
15
|
+
state?.product?.roadmap?.exists && !state?.product?.roadmap?.isBoilerplate;
|
|
16
|
+
|
|
17
|
+
if (!isProductComplete) {
|
|
18
|
+
step = {
|
|
19
|
+
phase: "Phase 1: Product Strategy",
|
|
20
|
+
title: "Plan Your Product",
|
|
21
|
+
description: "Define your Mission, Roadmap, and Tech Stack to build a solid foundation.",
|
|
22
|
+
prompt: "Antigravity, let's start Phase 1: Product Planning. Please read 'agent-os/commands/plan-product/plan-product.md' and guide me.",
|
|
23
|
+
link: "",
|
|
24
|
+
actionLabel: "",
|
|
25
|
+
action: () => { }
|
|
26
|
+
}
|
|
27
|
+
} else if (!state?.design?.initialized) {
|
|
28
|
+
step = {
|
|
29
|
+
phase: "Phase 2: Design System",
|
|
30
|
+
title: "Sync Product to Design OS",
|
|
31
|
+
description: "Transfer your Product Mission and Roadmap to Design OS to automate the setup.",
|
|
32
|
+
prompt: "Antigravity, please sync my product plan to Design OS. Read 'agent-os/commands/initialize-design/initialize-design.md'.",
|
|
33
|
+
link: "",
|
|
34
|
+
actionLabel: "",
|
|
35
|
+
action: () => { }
|
|
36
|
+
}
|
|
37
|
+
} else if (!state?.design?.exported) {
|
|
38
|
+
step = {
|
|
39
|
+
phase: "Phase 2: Design System",
|
|
40
|
+
title: "Define Your Visuals",
|
|
41
|
+
description: "Defining the visuals now prevents generic UI later. Export your system when ready.",
|
|
42
|
+
prompt: "",
|
|
43
|
+
link: "",
|
|
44
|
+
actionLabel: "Open Design OS",
|
|
45
|
+
action: onOpenDesign
|
|
46
|
+
};
|
|
47
|
+
} else if (!state?.implementation?.scaffolded) {
|
|
48
|
+
step = {
|
|
49
|
+
phase: "Phase 3: Implementation",
|
|
50
|
+
title: "Scaffold Application",
|
|
51
|
+
description: "Your design is exported. Now, scaffold the production app with the design system.",
|
|
52
|
+
prompt: "Antigravity, scaffold the implementation. Read 'agent-os/commands/scaffold-implementation/scaffold-implementation.md'.",
|
|
53
|
+
link: "",
|
|
54
|
+
actionLabel: "",
|
|
55
|
+
action: () => { }
|
|
56
|
+
};
|
|
57
|
+
} else if (state?.specs?.length === 0) {
|
|
58
|
+
step = {
|
|
59
|
+
phase: "Phase 4: Feature Specs",
|
|
60
|
+
title: "Shape Your First Spec",
|
|
61
|
+
description: "Your app is ready! Create a spec for the first feature in your roadmap.",
|
|
62
|
+
prompt: "Antigravity, let's shape the spec for a new feature. Please read 'agent-os/commands/shape-spec/shape-spec.md'.",
|
|
63
|
+
link: "",
|
|
64
|
+
actionLabel: "",
|
|
65
|
+
action: () => { }
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
const incompleteSpec = state?.specs?.find(s => s.tasks.completed < s.tasks.total || !s.tasks.exists);
|
|
69
|
+
if (incompleteSpec) {
|
|
70
|
+
step = {
|
|
71
|
+
phase: "Phase 4: Feature Specs",
|
|
72
|
+
title: `Implement '${incompleteSpec.name}'`,
|
|
73
|
+
description: `Active spec detected. Write the code!`,
|
|
74
|
+
prompt: `Antigravity, implement the tasks for '${incompleteSpec.name}'. Read commands/implement-tasks/implement-tasks.md.`,
|
|
75
|
+
link: "",
|
|
76
|
+
actionLabel: "",
|
|
77
|
+
action: () => { }
|
|
78
|
+
};
|
|
79
|
+
} else if (state?.product?.roadmap?.nextItem && !state?.product?.roadmap?.isBoilerplate) {
|
|
80
|
+
const rawName = state?.product?.roadmap?.nextItem;
|
|
81
|
+
const cleanName = rawName.replace(/\*\*/g, '').replace(/^\d+\.\s*/, '').split('\n')[0].trim();
|
|
82
|
+
step = {
|
|
83
|
+
phase: "Phase 4: Feature Specs",
|
|
84
|
+
title: `Shape '${cleanName}'`,
|
|
85
|
+
description: `Ready to start the next feature? Shape the spec now.`,
|
|
86
|
+
prompt: `Antigravity, let's shape the spec for '${cleanName}'. Please read 'agent-os/commands/shape-spec/shape-spec.md'.`,
|
|
87
|
+
link: "",
|
|
88
|
+
actionLabel: "",
|
|
89
|
+
action: () => { }
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
step = {
|
|
93
|
+
phase: "Phase 4: Feature Specs",
|
|
94
|
+
title: "Verify or Plan Next",
|
|
95
|
+
description: "Verify implementation or start a new feature.",
|
|
96
|
+
prompt: "Antigravity, let's verify the implementation. Read 'agent-os/commands/implement-tasks/3-verify-implementation.md'.",
|
|
97
|
+
link: "",
|
|
98
|
+
actionLabel: "",
|
|
99
|
+
action: () => { }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Guidance
|
|
106
|
+
phase={step.phase}
|
|
107
|
+
title={step.title}
|
|
108
|
+
description={step.description}
|
|
109
|
+
prompt={step.prompt}
|
|
110
|
+
actionLabel={step.actionLabel}
|
|
111
|
+
onAction={step.action}
|
|
112
|
+
className="mb-8"
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Copy, Play } from 'lucide-react';
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { IdeContext } from '../contexts/IdeContext';
|
|
4
|
+
|
|
5
|
+
interface PromptButtonProps {
|
|
6
|
+
label: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
onClick: (text: string) => void;
|
|
9
|
+
small?: boolean;
|
|
10
|
+
primary?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PromptButton({ label, prompt, onClick, small, primary }: PromptButtonProps) {
|
|
14
|
+
const { scheme } = useContext(IdeContext);
|
|
15
|
+
const deepLink = `${scheme}://vscode.executeCommand/workbench.action.chat.open?query=${encodeURIComponent(prompt)}`;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={`flex gap-1 items-center ${!small ? 'w-full' : 'flex-1'}`}>
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => onClick(prompt)}
|
|
21
|
+
className={`flex items-center justify-center gap-2 rounded-lg font-medium transition cursor-pointer flex-1
|
|
22
|
+
${small ? 'px-3 py-1.5 text-xs' : 'px-4 py-2 text-sm'}
|
|
23
|
+
${primary
|
|
24
|
+
? 'bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm'
|
|
25
|
+
: 'bg-background hover:bg-secondary border border-border text-foreground hover:text-foreground'}
|
|
26
|
+
`}
|
|
27
|
+
>
|
|
28
|
+
<Copy size={small ? 12 : 14} />
|
|
29
|
+
{label}
|
|
30
|
+
</button>
|
|
31
|
+
|
|
32
|
+
<a
|
|
33
|
+
href={deepLink}
|
|
34
|
+
className={`flex items-center justify-center rounded-lg border border-border bg-secondary hover:bg-secondary/80 text-foreground transition
|
|
35
|
+
${small ? 'w-8 h-[30px]' : 'w-10 h-[38px]'}
|
|
36
|
+
`}
|
|
37
|
+
title={`Run in IDE (${scheme}://)`}
|
|
38
|
+
>
|
|
39
|
+
<Play size={small ? 12 : 14} fill="currentColor" className="opacity-70" />
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Status } from '../types';
|
|
2
|
+
|
|
3
|
+
interface StatusItemProps {
|
|
4
|
+
label: string;
|
|
5
|
+
status?: Status;
|
|
6
|
+
icon: React.ReactNode;
|
|
7
|
+
small?: boolean;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function StatusItem({ label, status, icon, small, onClick }: StatusItemProps) {
|
|
12
|
+
if (!status) return null;
|
|
13
|
+
const isComplete = status.exists && (status.total > 0 ? status.completed === status.total : true);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
onClick={onClick}
|
|
18
|
+
className={`flex items-center justify-between ${small ? 'text-xs' : 'text-sm'} ${onClick ? 'cursor-pointer hover:bg-secondary/50 p-1.5 -mx-1.5 rounded-md transition-colors group' : ''}`}
|
|
19
|
+
>
|
|
20
|
+
<div className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
|
|
21
|
+
{icon}
|
|
22
|
+
<span>{label}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
{status.exists && !status.isBoilerplate ? (
|
|
26
|
+
<span className={`px-2 py-0.5 rounded-full text-xs font-medium border ${isComplete
|
|
27
|
+
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 border-emerald-200 dark:border-emerald-800'
|
|
28
|
+
: 'bg-secondary text-muted-foreground border-border'
|
|
29
|
+
}`}>
|
|
30
|
+
{isComplete ? 'Done' : 'Pending'}
|
|
31
|
+
</span>
|
|
32
|
+
) : (
|
|
33
|
+
<span className="text-muted-foreground bg-secondary px-2 py-0.5 rounded text-xs font-medium border border-border">Pending</span>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useToast } from '../../components/ui/ToastContext';
|
|
4
|
+
|
|
5
|
+
interface CreateSpecModalProps {
|
|
6
|
+
runtimeConfig: any;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onSuccess: () => void;
|
|
9
|
+
openFile: (path: string, title: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const CreateSpecModal = ({ runtimeConfig, onClose, onSuccess, openFile }: CreateSpecModalProps) => {
|
|
13
|
+
const [newSpecName, setNewSpecName] = useState("");
|
|
14
|
+
const { toast } = useToast();
|
|
15
|
+
|
|
16
|
+
const handleCreate = async () => {
|
|
17
|
+
if (newSpecName.trim()) {
|
|
18
|
+
const name = newSpecName.trim();
|
|
19
|
+
if (runtimeConfig?.api) {
|
|
20
|
+
await axios.post(`${runtimeConfig.api}/api/scaffold/spec`, { name });
|
|
21
|
+
const prompt = `Antigravity, let's shape the spec for '${name}'. Read commands/shape-spec/shape-spec.md.`;
|
|
22
|
+
navigator.clipboard.writeText(prompt);
|
|
23
|
+
setNewSpecName("");
|
|
24
|
+
onSuccess();
|
|
25
|
+
openFile(`specs/${name}/spec.md`, `${name} Spec`);
|
|
26
|
+
toast({ title: "Spec created & Prompt copied!", type: 'success' });
|
|
27
|
+
onClose();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="fixed inset-0 z-[200] bg-black/50 backdrop-blur-sm flex items-center justify-center p-6 animate-in fade-in">
|
|
34
|
+
<div className="bg-card border border-border w-full max-w-md rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
|
|
35
|
+
<div className="px-6 py-4 border-b border-border bg-muted/30">
|
|
36
|
+
<h3 className="font-semibold text-lg">New Feature Spec</h3>
|
|
37
|
+
<p className="text-sm text-muted-foreground">Shape a new feature for your project.</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="p-6">
|
|
40
|
+
<div className="space-y-4">
|
|
41
|
+
<div>
|
|
42
|
+
<label className="text-sm font-medium mb-1.5 block">Feature Name</label>
|
|
43
|
+
<input
|
|
44
|
+
autoFocus
|
|
45
|
+
type="text"
|
|
46
|
+
className="w-full px-3 py-2 rounded-md border border-input bg-secondary text-secondary-foreground text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
47
|
+
placeholder="e.g. user-profile"
|
|
48
|
+
value={newSpecName}
|
|
49
|
+
onChange={(e) => setNewSpecName(e.target.value)}
|
|
50
|
+
onKeyDown={(e) => {
|
|
51
|
+
if (e.key === 'Enter') handleCreate();
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
<p className="text-xs text-muted-foreground mt-1.5">
|
|
55
|
+
This will create a new directory in <code>specs/</code> and copy the prompt to your clipboard.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
59
|
+
<button onClick={onClose} className="px-4 py-2 text-sm font-medium rounded-md hover:bg-secondary transition-colors">Cancel</button>
|
|
60
|
+
<button
|
|
61
|
+
disabled={!newSpecName.trim()}
|
|
62
|
+
onClick={handleCreate}
|
|
63
|
+
className="px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
64
|
+
>
|
|
65
|
+
Create Spec
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Trash2 } from 'lucide-react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
interface DeleteSpecModalProps {
|
|
5
|
+
specName: string;
|
|
6
|
+
runtimeConfig: any;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onSuccess: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DeleteSpecModal = ({ specName, runtimeConfig, onClose, onSuccess }: DeleteSpecModalProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className="fixed inset-0 z-[200] bg-black/50 backdrop-blur-sm flex items-center justify-center p-6 animate-in fade-in">
|
|
14
|
+
<div className="bg-card border border-border w-full max-w-sm rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
|
|
15
|
+
<div className="p-6">
|
|
16
|
+
<div className="flex items-center gap-3 mb-4 text-red-500">
|
|
17
|
+
<div className="p-2 bg-red-100 dark:bg-red-900/20 rounded-full">
|
|
18
|
+
<Trash2 size={24} />
|
|
19
|
+
</div>
|
|
20
|
+
<h3 className="font-semibold text-lg text-foreground">Delete Spec?</h3>
|
|
21
|
+
</div>
|
|
22
|
+
<p className="text-muted-foreground mb-6">
|
|
23
|
+
Are you sure you want to delete <strong>{specName}</strong>? This action cannot be undone and will permanently delete the spec files.
|
|
24
|
+
</p>
|
|
25
|
+
<div className="flex justify-end gap-2">
|
|
26
|
+
<button onClick={onClose} className="px-4 py-2 text-sm font-medium rounded-md hover:bg-secondary transition-colors">Cancel</button>
|
|
27
|
+
<button
|
|
28
|
+
onClick={async () => {
|
|
29
|
+
await axios.delete(`${runtimeConfig.api}/api/specs/${specName}`);
|
|
30
|
+
onSuccess();
|
|
31
|
+
onClose();
|
|
32
|
+
}}
|
|
33
|
+
className="px-4 py-2 text-sm font-medium rounded-md bg-red-500 hover:bg-red-600 text-white transition-colors"
|
|
34
|
+
>
|
|
35
|
+
Delete Spec
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { FileText, X } from 'lucide-react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkBreaks from 'remark-breaks';
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
interface FileEditorModalProps {
|
|
8
|
+
viewingFile: { path: string, title: string };
|
|
9
|
+
content: string | null;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onSave: (content: string) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FileEditorModal = ({ viewingFile, content, onClose, onSave }: FileEditorModalProps) => {
|
|
15
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
16
|
+
const [editContent, setEditContent] = useState("");
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (content) setEditContent(content);
|
|
20
|
+
}, [content]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="fixed inset-0 z-[200] bg-black/50 backdrop-blur-sm flex items-center justify-center p-6 animate-in fade-in">
|
|
24
|
+
<div className="bg-card border border-border w-full max-w-4xl max-h-[90vh] rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
|
|
25
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border bg-muted/30">
|
|
26
|
+
<div className="flex items-center gap-3">
|
|
27
|
+
<div className="p-2 bg-primary/10 text-primary rounded-md">
|
|
28
|
+
<FileText size={18} />
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<h3 className="font-semibold text-lg">{viewingFile.title}</h3>
|
|
32
|
+
<code className="text-xs text-muted-foreground font-mono">{viewingFile.path}</code>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
{!isEditing ? (
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => setIsEditing(true)}
|
|
39
|
+
className="px-3 py-1.5 text-sm font-medium bg-secondary hover:bg-secondary/80 rounded-md transition-colors"
|
|
40
|
+
>
|
|
41
|
+
Edit
|
|
42
|
+
</button>
|
|
43
|
+
) : (
|
|
44
|
+
<>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => setIsEditing(false)}
|
|
47
|
+
className="px-3 py-1.5 text-sm font-medium hover:bg-secondary rounded-md transition-colors"
|
|
48
|
+
>
|
|
49
|
+
Cancel
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
onClick={async () => {
|
|
53
|
+
await onSave(editContent);
|
|
54
|
+
setIsEditing(false);
|
|
55
|
+
}}
|
|
56
|
+
className="px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 rounded-md transition-colors"
|
|
57
|
+
>
|
|
58
|
+
Save Changes
|
|
59
|
+
</button>
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
<button onClick={onClose} className="p-2 hover:bg-secondary rounded-full transition-colors ml-2">
|
|
63
|
+
<X size={20} />
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex-1 overflow-y-auto p-6 bg-background">
|
|
68
|
+
{content === "Loading..." || content === "Error loading file." ? (
|
|
69
|
+
<div className="font-mono text-sm">{content}</div>
|
|
70
|
+
) : isEditing ? (
|
|
71
|
+
<textarea
|
|
72
|
+
value={editContent}
|
|
73
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
74
|
+
className="w-full h-full min-h-[500px] p-4 font-mono text-sm bg-secondary text-secondary-foreground border border-border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
75
|
+
/>
|
|
76
|
+
) : (
|
|
77
|
+
<article className="prose dark:prose-invert max-w-none prose-headings:font-serif prose-headings:font-semibold prose-a:text-primary prose-code:text-primary prose-pre:bg-secondary/50 prose-pre:border prose-pre:border-border prose-p:leading-relaxed prose-p:mb-4">
|
|
78
|
+
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>
|
|
79
|
+
{content || ''}
|
|
80
|
+
</ReactMarkdown>
|
|
81
|
+
</article>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { X, Settings } from 'lucide-react';
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { IdeContext } from '../../contexts/IdeContext';
|
|
4
|
+
|
|
5
|
+
interface SettingsModalProps {
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const SettingsModal = ({ onClose }: SettingsModalProps) => {
|
|
10
|
+
const { scheme, setScheme } = useContext(IdeContext);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="fixed inset-0 z-[200] bg-black/50 backdrop-blur-sm flex items-center justify-center p-6 animate-in fade-in">
|
|
14
|
+
<div className="bg-card border border-border w-full max-w-sm rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
|
|
15
|
+
<div className="p-6 border-b border-border bg-muted/30 flex items-center justify-between">
|
|
16
|
+
<h3 className="font-semibold text-lg flex items-center gap-2"><Settings size={18} /> Settings</h3>
|
|
17
|
+
<button onClick={onClose} className="hover:bg-secondary p-1 rounded"><X size={18} /></button>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="p-6 space-y-4">
|
|
20
|
+
<div>
|
|
21
|
+
<label className="text-sm font-medium mb-2 block">IDE / Editor</label>
|
|
22
|
+
<div className="space-y-2">
|
|
23
|
+
{['vscode', 'cursor', 'windsurf', 'antigravity'].map((s) => (
|
|
24
|
+
<button
|
|
25
|
+
key={s}
|
|
26
|
+
onClick={() => setScheme(s)}
|
|
27
|
+
className={`w-full flex items-center justify-between px-4 py-3 rounded-lg border transition-all ${scheme === s
|
|
28
|
+
? 'bg-primary/10 border-primary text-primary'
|
|
29
|
+
: 'bg-secondary/20 border-border hover:border-foreground/20'
|
|
30
|
+
}`}
|
|
31
|
+
>
|
|
32
|
+
<span className="capitalize font-medium">{s}</span>
|
|
33
|
+
{scheme === s && <div className="w-2 h-2 rounded-full bg-primary" />}
|
|
34
|
+
</button>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
<p className="text-xs text-muted-foreground mt-3">
|
|
38
|
+
Select the editor to open when clicking "Play".
|
|
39
|
+
<br />Currently using: <code>{scheme}://</code>
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
|
2
2
|
import { X, CheckCircle, Info, AlertTriangle } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { useToast } from '../components/ui/ToastContext';
|
|
4
|
+
|
|
5
|
+
export const useFileEditor = (runtimeConfig: any, fetchStatus: () => void) => {
|
|
6
|
+
const [viewingFile, setViewingFile] = useState<{ path: string, title: string } | null>(null);
|
|
7
|
+
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
8
|
+
const { toast } = useToast();
|
|
9
|
+
|
|
10
|
+
const openFile = async (path: string, title: string) => {
|
|
11
|
+
setViewingFile({ path, title });
|
|
12
|
+
setFileContent("Loading...");
|
|
13
|
+
try {
|
|
14
|
+
const res = await axios.get(`${runtimeConfig.api}/api/files?path=${encodeURIComponent(path)}`);
|
|
15
|
+
setFileContent(res.data.content);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
setFileContent("Error loading file.");
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const saveFile = async (content: string) => {
|
|
22
|
+
if (!viewingFile || !runtimeConfig) return;
|
|
23
|
+
try {
|
|
24
|
+
await axios.post(`${runtimeConfig.api}/api/files`, {
|
|
25
|
+
path: viewingFile.path,
|
|
26
|
+
content: content
|
|
27
|
+
});
|
|
28
|
+
setFileContent(content);
|
|
29
|
+
toast({ title: "File Saved!", type: "success" });
|
|
30
|
+
fetchStatus();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
toast({ title: "Failed to save file", type: "error" });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const closeFile = () => {
|
|
37
|
+
setViewingFile(null);
|
|
38
|
+
setFileContent(null);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return { viewingFile, fileContent, openFile, saveFile, closeFile };
|
|
42
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import type { ProjectState } from '../types';
|
|
4
|
+
|
|
5
|
+
export const useProjectState = () => {
|
|
6
|
+
const [state, setState] = useState<ProjectState | null>(null);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
const [runtimeConfig, setRuntimeConfig] = useState<any>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
fetch('/runtime-config.json')
|
|
12
|
+
.then(res => res.json())
|
|
13
|
+
.then(config => { setRuntimeConfig(config); })
|
|
14
|
+
.catch(() => {
|
|
15
|
+
setRuntimeConfig({
|
|
16
|
+
api: 'http://localhost:5403',
|
|
17
|
+
app: 'http://localhost:5402',
|
|
18
|
+
design: 'http://localhost:5400',
|
|
19
|
+
ports: { api: 5403, app: 5402, design: 5400 }
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const fetchStatus = async () => {
|
|
25
|
+
if (!runtimeConfig) return;
|
|
26
|
+
try {
|
|
27
|
+
const res = await axios.get(`${runtimeConfig.api}/api/status`);
|
|
28
|
+
setState(res.data);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(error);
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (runtimeConfig) {
|
|
38
|
+
fetchStatus();
|
|
39
|
+
const interval = setInterval(fetchStatus, 2000);
|
|
40
|
+
return () => clearInterval(interval);
|
|
41
|
+
}
|
|
42
|
+
}, [runtimeConfig]);
|
|
43
|
+
|
|
44
|
+
return { state, loading, runtimeConfig, fetchStatus };
|
|
45
|
+
};
|