summaryrefslogtreecommitdiff
path: root/makima/frontend/src
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-02 02:34:50 +0000
committersoryu <soryu@soryu.co>2026-02-02 02:34:50 +0000
commit151e9d87e117b7980e6aad522ac8f3633eeca87a (patch)
treee80fb4301361b3b12e5abf8e442603db2d0622dc /makima/frontend/src
parenta2c147ddd59f55a07b5be0c8970169726b55c876 (diff)
downloadsoryu-151e9d87e117b7980e6aad522ac8f3633eeca87a.tar.gz
soryu-151e9d87e117b7980e6aad522ac8f3633eeca87a.zip
Make makima more opinionated and structured
Diffstat (limited to 'makima/frontend/src')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/components/contracts/ContractList.tsx5
-rw-r--r--makima/frontend/src/components/templates/TemplateEditor.tsx257
-rw-r--r--makima/frontend/src/lib/api.ts110
-rw-r--r--makima/frontend/src/main.tsx9
-rw-r--r--makima/frontend/src/routes/contracts.tsx58
-rw-r--r--makima/frontend/src/routes/templates.tsx388
-rw-r--r--makima/frontend/src/types/templates.ts90
8 files changed, 2 insertions, 916 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index f44799b..fb95c7f 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -14,7 +14,6 @@ const NAV_LINKS: NavLink[] = [
{ label: "Board", href: "/workflow", requiresAuth: true },
{ label: "Mesh", href: "/mesh", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
- { label: "Templates", href: "/templates", requiresAuth: true },
];
export function NavStrip() {
diff --git a/makima/frontend/src/components/contracts/ContractList.tsx b/makima/frontend/src/components/contracts/ContractList.tsx
index 532ab87..98f8ff6 100644
--- a/makima/frontend/src/components/contracts/ContractList.tsx
+++ b/makima/frontend/src/components/contracts/ContractList.tsx
@@ -136,11 +136,6 @@ export function ContractList({
Local
</span>
)}
- {contract.redTeamEnabled && (
- <span className="px-1.5 py-0.5 font-mono text-[9px] uppercase text-cyan-400 border border-cyan-400/30 bg-cyan-400/10 shrink-0" title="Red Team monitoring enabled">
- 🔍 Red Team
- </span>
- )}
</div>
<span
className={`text-[10px] font-mono uppercase shrink-0 ${
diff --git a/makima/frontend/src/components/templates/TemplateEditor.tsx b/makima/frontend/src/components/templates/TemplateEditor.tsx
deleted file mode 100644
index c8e1f98..0000000
--- a/makima/frontend/src/components/templates/TemplateEditor.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-import { useState } from "react";
-import type { ContractTemplate, Phase, Deliverable } from "../../types/templates";
-
-interface Props {
- template: ContractTemplate;
- onSave: (template: ContractTemplate) => void;
- onCancel: () => void;
- readOnly?: boolean;
-}
-
-export function TemplateEditor({ template, onSave, onCancel, readOnly = false }: Props) {
- const [editedTemplate, setEditedTemplate] = useState<ContractTemplate>({
- ...template,
- phases: template.phases.map((p) => ({
- ...p,
- deliverables: [...p.deliverables],
- })),
- });
- const [newDeliverableName, setNewDeliverableName] = useState<{
- [phaseId: string]: string;
- }>({});
-
- const handlePhaseNameChange = (phaseId: string, newName: string) => {
- setEditedTemplate((prev) => ({
- ...prev,
- phases: prev.phases.map((p) =>
- p.id === phaseId ? { ...p, name: newName } : p
- ),
- }));
- };
-
- const handleDeliverableNameChange = (
- phaseId: string,
- deliverableId: string,
- newName: string
- ) => {
- setEditedTemplate((prev) => ({
- ...prev,
- phases: prev.phases.map((p) =>
- p.id === phaseId
- ? {
- ...p,
- deliverables: p.deliverables.map((d) =>
- d.id === deliverableId ? { ...d, name: newName } : d
- ),
- }
- : p
- ),
- }));
- };
-
- const handleAddDeliverable = (phaseId: string) => {
- const name = newDeliverableName[phaseId]?.trim();
- if (!name) return;
-
- const newDeliverable: Deliverable = {
- id: `deliverable-${Date.now()}`,
- name,
- };
-
- setEditedTemplate((prev) => ({
- ...prev,
- phases: prev.phases.map((p) =>
- p.id === phaseId
- ? { ...p, deliverables: [...p.deliverables, newDeliverable] }
- : p
- ),
- }));
- setNewDeliverableName((prev) => ({ ...prev, [phaseId]: "" }));
- };
-
- const handleRemoveDeliverable = (phaseId: string, deliverableId: string) => {
- setEditedTemplate((prev) => ({
- ...prev,
- phases: prev.phases.map((p) =>
- p.id === phaseId
- ? {
- ...p,
- deliverables: p.deliverables.filter((d) => d.id !== deliverableId),
- }
- : p
- ),
- }));
- };
-
- const handleAddPhase = () => {
- const newPhase: Phase = {
- id: `phase-${Date.now()}`,
- name: "New Phase",
- deliverables: [],
- };
- setEditedTemplate((prev) => ({
- ...prev,
- phases: [...prev.phases, newPhase],
- }));
- };
-
- const handleRemovePhase = (phaseId: string) => {
- setEditedTemplate((prev) => ({
- ...prev,
- phases: prev.phases.filter((p) => p.id !== phaseId),
- }));
- };
-
- return (
- <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6">
- {/* Header */}
- <div className="mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]">
- <h2 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff] mb-1">
- {readOnly ? "View" : "Edit"} Template: {template.name}
- </h2>
- <p className="text-xs font-mono text-[#75aafc] opacity-70">
- {template.description}
- </p>
- {readOnly && (
- <p className="text-xs font-mono text-amber-400 mt-2">
- Built-in templates are read-only
- </p>
- )}
- </div>
-
- {/* Phases */}
- <div className="space-y-4 mb-6">
- {editedTemplate.phases.map((phase, phaseIndex) => (
- <div
- key={phase.id}
- className="bg-[rgba(0,0,0,0.3)] border border-[rgba(117,170,252,0.2)] p-4"
- >
- {/* Phase Header */}
- <div className="flex items-center gap-3 mb-3">
- <span className="w-6 h-6 flex items-center justify-center bg-[rgba(117,170,252,0.2)] text-[#9bc3ff] text-xs font-mono">
- {phaseIndex + 1}
- </span>
- <input
- type="text"
- className="flex-1 px-3 py-1.5 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3] disabled:opacity-60"
- value={phase.name}
- onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)}
- placeholder="Phase name"
- disabled={readOnly}
- />
- {!template.isBuiltIn && (
- <button
- type="button"
- onClick={() => handleRemovePhase(phase.id)}
- className="w-7 h-7 flex items-center justify-center border border-[rgba(255,100,100,0.3)] text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-sm"
- title="Remove phase"
- >
- x
- </button>
- )}
- </div>
-
- {/* Deliverables */}
- <div className="ml-9 space-y-2">
- {phase.deliverables.length === 0 ? (
- <div className="text-xs font-mono text-[#556677] italic">
- No deliverables
- </div>
- ) : (
- phase.deliverables.map((deliverable) => (
- <div
- key={deliverable.id}
- className="flex items-center gap-2"
- >
- <span className="text-[#75aafc] text-xs">-</span>
- <input
- type="text"
- className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#3f6fb3]"
- value={deliverable.name}
- onChange={(e) =>
- handleDeliverableNameChange(
- phase.id,
- deliverable.id,
- e.target.value
- )
- }
- />
- <button
- type="button"
- onClick={() =>
- handleRemoveDeliverable(phase.id, deliverable.id)
- }
- className="w-5 h-5 flex items-center justify-center text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-xs"
- title="Remove deliverable"
- >
- x
- </button>
- </div>
- ))
- )}
-
- {/* Add Deliverable */}
- <div className="flex items-center gap-2 pt-2">
- <input
- type="text"
- className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs placeholder-[#445566] focus:outline-none focus:border-[#3f6fb3]"
- placeholder="New deliverable name..."
- value={newDeliverableName[phase.id] || ""}
- onChange={(e) =>
- setNewDeliverableName((prev) => ({
- ...prev,
- [phase.id]: e.target.value,
- }))
- }
- onKeyPress={(e) => {
- if (e.key === "Enter") {
- handleAddDeliverable(phase.id);
- }
- }}
- />
- <button
- type="button"
- onClick={() => handleAddDeliverable(phase.id)}
- className="px-2 py-1 border border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.1)] transition-colors"
- >
- + Add
- </button>
- </div>
- </div>
- </div>
- ))}
- </div>
-
- {/* Add Phase (only for custom templates) */}
- {!template.isBuiltIn && (
- <button
- type="button"
- onClick={handleAddPhase}
- className="w-full mb-6 px-4 py-2 border border-dashed border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors"
- >
- + Add Phase
- </button>
- )}
-
- {/* Footer Actions */}
- <div className="flex gap-3 justify-end pt-4 border-t border-[rgba(117,170,252,0.15)]">
- <button
- type="button"
- onClick={onCancel}
- className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors"
- >
- {readOnly ? "Close" : "Cancel"}
- </button>
- {!readOnly && (
- <button
- type="button"
- onClick={() => onSave(editedTemplate)}
- className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors"
- >
- Save Changes
- </button>
- )}
- </div>
- </div>
- );
-}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index e8b3d8a..f148d76 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1659,45 +1659,9 @@ export interface DeliverableDefinition {
priority: "required" | "recommended" | "optional";
}
-/** Request to create a custom contract type template */
-export interface CreateTemplateRequest {
- name: string;
- description?: string;
- phases: PhaseDefinition[];
- defaultPhase: string;
- deliverables?: Record<string, DeliverableDefinition[]>;
-}
-
-/** Request to update a custom contract type template */
-export interface UpdateTemplateRequest {
- name?: string;
- description?: string;
- phases?: PhaseDefinition[];
- defaultPhase?: string;
- deliverables?: Record<string, DeliverableDefinition[]>;
- version?: number;
-}
-
-/** Custom template record from the API */
-export interface ContractTypeTemplateRecord {
- id: string;
- name: string;
- description: string | null;
- phases: PhaseDefinition[];
- defaultPhase: string;
- isBuiltin: boolean;
- version: number;
- createdAt: string;
-}
-
-/** Response for single template operations */
-export interface TemplateResponse {
- template: ContractTypeTemplateRecord;
-}
-
/**
- * List available contract types/templates.
- * Returns built-in types (simple, specification) and any custom types.
+ * List available contract types.
+ * Returns built-in types only (simple, specification, execute).
*/
export async function listContractTypes(): Promise<ListContractTypesResponse> {
const res = await authFetch(`${API_BASE}/api/v1/contract-types`);
@@ -1707,66 +1671,6 @@ export async function listContractTypes(): Promise<ListContractTypesResponse> {
return res.json();
}
-/**
- * Create a new custom contract type template.
- */
-export async function createContractTemplate(
- req: CreateTemplateRequest
-): Promise<TemplateResponse> {
- const res = await authFetch(`${API_BASE}/api/v1/contract-types`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(req),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({ message: res.statusText }));
- throw new Error(err.message || `Failed to create template: ${res.statusText}`);
- }
- return res.json();
-}
-
-/**
- * Get a custom contract type template by ID.
- */
-export async function getContractTemplate(id: string): Promise<TemplateResponse> {
- const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`);
- if (!res.ok) {
- throw new Error(`Failed to get template: ${res.statusText}`);
- }
- return res.json();
-}
-
-/**
- * Update a custom contract type template.
- */
-export async function updateContractTemplate(
- id: string,
- req: UpdateTemplateRequest
-): Promise<TemplateResponse> {
- const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(req),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({ message: res.statusText }));
- throw new Error(err.message || `Failed to update template: ${res.statusText}`);
- }
- return res.json();
-}
-
-/**
- * Delete a custom contract type template.
- */
-export async function deleteContractTemplate(id: string): Promise<void> {
- const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`, {
- method: "DELETE",
- });
- if (!res.ok) {
- throw new Error(`Failed to delete template: ${res.statusText}`);
- }
-}
-
export interface ContractRepository {
id: string;
contractId: string;
@@ -1792,8 +1696,6 @@ export interface ContractSummary {
supervisorTaskId: string | null;
/** When true, tasks won't auto-push or create PRs - use patch files instead */
localOnly: boolean;
- /** When true, a red team task monitors work output for quality */
- redTeamEnabled: boolean;
fileCount: number;
taskCount: number;
repositoryCount: number;
@@ -1818,10 +1720,6 @@ export interface Contract {
phaseGuard: boolean;
/** When true, tasks won't auto-push or create PRs - use patch files instead */
localOnly: boolean;
- /** When true, a red team task monitors work output for quality */
- redTeamEnabled: boolean;
- /** Custom criteria for the red team to evaluate */
- redTeamPrompt: string | null;
version: number;
createdAt: string;
updatedAt: string;
@@ -1859,10 +1757,6 @@ export interface CreateContractRequest {
initialPhase?: ContractPhase | string;
/** When true, tasks won't auto-push or create PRs - use patch files instead */
localOnly?: boolean;
- /** When true, spawn a red team task to monitor work output */
- redTeamEnabled?: boolean;
- /** Custom criteria for the red team to evaluate */
- redTeamPrompt?: string;
}
export interface UpdateContractRequest {
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index ef1ba5c..50fffe4 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -18,7 +18,6 @@ import HistoryPage from "./routes/history";
import LoginPage from "./routes/login";
import SettingsPage from "./routes/settings";
import ContractFilePage from "./routes/contract-file";
-import TemplatesPage from "./routes/templates";
import SpeakPage from "./routes/speak";
createRoot(document.getElementById("root")!).render(
@@ -129,14 +128,6 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
- path="/templates"
- element={
- <ProtectedRoute>
- <TemplatesPage />
- </ProtectedRoute>
- }
- />
- <Route
path="/speak"
element={
<ProtectedRoute>
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
index 8dcfe34..dde78b1 100644
--- a/makima/frontend/src/routes/contracts.tsx
+++ b/makima/frontend/src/routes/contracts.tsx
@@ -93,8 +93,6 @@ function ContractsPageContent() {
const [contractTypes, setContractTypes] = useState<ContractTypeTemplate[]>([]);
const [contractTypesLoading, setContractTypesLoading] = useState(false);
const [localOnly, setLocalOnly] = useState(false);
- const [redTeamEnabled, setRedTeamEnabled] = useState(false);
- const [redTeamPrompt, setRedTeamPrompt] = useState("");
// Fetch contract types when modal opens - API returns both built-in and custom templates
useEffect(() => {
@@ -238,8 +236,6 @@ function ContractsPageContent() {
templateId: isCustomTemplate ? contractType : undefined,
initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined,
localOnly: localOnly || undefined,
- redTeamEnabled: redTeamEnabled || undefined,
- redTeamPrompt: redTeamEnabled && redTeamPrompt.trim() ? redTeamPrompt.trim() : undefined,
};
try {
@@ -315,8 +311,6 @@ function ContractsPageContent() {
setRepoUrl("");
setRepoPath("");
setLocalOnly(false);
- setRedTeamEnabled(false);
- setRedTeamPrompt("");
setCreateError(null);
}, []);
@@ -689,58 +683,6 @@ function ContractsPageContent() {
</div>
{/* Red Team Monitoring */}
- <div className="border-t border-[rgba(117,170,252,0.2)] pt-4">
- <div className="flex items-center gap-3">
- <button
- type="button"
- onClick={() => setRedTeamEnabled(!redTeamEnabled)}
- className={`w-5 h-5 flex items-center justify-center border transition-colors ${
- redTeamEnabled
- ? "bg-[#0f3c78] border-[#75aafc] text-[#dbe7ff]"
- : "bg-[#0d1b2d] border-[#3f6fb3] text-transparent"
- }`}
- >
- {redTeamEnabled && (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="12"
- height="12"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="3"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <polyline points="20 6 9 17 4 12" />
- </svg>
- )}
- </button>
- <label
- className="font-mono text-sm text-[#dbe7ff] cursor-pointer select-none"
- onClick={() => setRedTeamEnabled(!redTeamEnabled)}
- >
- Enable Red Team Monitoring
- </label>
- </div>
- <p className="font-mono text-xs text-[#8b949e] pl-8">
- Spawns a parallel task to monitor work output for quality and compliance.
- </p>
- {redTeamEnabled && (
- <div className="mt-3 pl-8">
- <label className="block font-mono text-xs text-[#75aafc] uppercase mb-2">
- Custom Review Criteria (Optional)
- </label>
- <textarea
- value={redTeamPrompt}
- onChange={(e) => setRedTeamPrompt(e.target.value)}
- placeholder="e.g., 'Focus on security best practices' or 'Ensure all functions have tests'"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] text-sm font-mono h-20 resize-none focus:border-[#75aafc] focus:outline-none"
- />
- </div>
- )}
- </div>
-
{/* Repository Configuration */}
<div className="border-t border-[rgba(117,170,252,0.2)] pt-4">
<label className="block font-mono text-xs text-[#75aafc] uppercase mb-3">
diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx
deleted file mode 100644
index b2c9974..0000000
--- a/makima/frontend/src/routes/templates.tsx
+++ /dev/null
@@ -1,388 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { useNavigate } from "react-router";
-import { Masthead } from "../components/Masthead";
-import { TemplateEditor } from "../components/templates/TemplateEditor";
-import { useAuth } from "../contexts/AuthContext";
-import type { ContractTemplate } from "../types/templates";
-import { DEFAULT_TEMPLATES } from "../types/templates";
-import {
- listContractTypes,
- createContractTemplate,
- updateContractTemplate,
- deleteContractTemplate,
- type PhaseDefinition,
- type DeliverableDefinition,
-} from "../lib/api";
-
-export default function TemplatesPage() {
- const navigate = useNavigate();
- const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
-
- const [templates, setTemplates] = useState<ContractTemplate[]>(DEFAULT_TEMPLATES);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const [saving, setSaving] = useState(false);
-
- const [editingTemplate, setEditingTemplate] = useState<ContractTemplate | null>(
- null
- );
- const [showNewTemplateForm, setShowNewTemplateForm] = useState(false);
- const [newTemplateName, setNewTemplateName] = useState("");
- const [newTemplateDescription, setNewTemplateDescription] = useState("");
-
- // Redirect to login if not authenticated
- useEffect(() => {
- if (!authLoading && isAuthConfigured && !isAuthenticated) {
- navigate("/login");
- }
- }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
-
- // Fetch templates from API
- const fetchTemplates = useCallback(async () => {
- try {
- setLoading(true);
- setError(null);
- const response = await listContractTypes();
-
- // Convert API response to ContractTemplate format
- const apiTemplates: ContractTemplate[] = response.contractTypes.map((t) => ({
- id: t.id,
- name: t.name,
- description: t.description,
- isBuiltIn: t.isBuiltin,
- phases: t.phases.map((phaseId) => ({
- id: phaseId,
- name: t.phaseNames?.[phaseId] || phaseId.charAt(0).toUpperCase() + phaseId.slice(1),
- deliverables: [], // Deliverables are managed server-side
- })),
- }));
-
- // Merge with DEFAULT_TEMPLATES to ensure we have full phase/deliverable info for built-ins
- const mergedTemplates = apiTemplates.map((apiTemplate) => {
- const defaultTemplate = DEFAULT_TEMPLATES.find((d) => d.id === apiTemplate.id);
- if (defaultTemplate && apiTemplate.isBuiltIn) {
- return defaultTemplate; // Use the richer default template for built-ins
- }
- return apiTemplate;
- });
-
- setTemplates(mergedTemplates);
- } catch (err) {
- console.error("Failed to fetch templates:", err);
- setError(err instanceof Error ? err.message : "Failed to fetch templates");
- // Fall back to default templates
- setTemplates(DEFAULT_TEMPLATES);
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- if (!authLoading && isAuthenticated) {
- fetchTemplates();
- } else if (!authLoading && !isAuthConfigured) {
- // No auth configured, just show defaults
- setTemplates(DEFAULT_TEMPLATES);
- setLoading(false);
- }
- }, [authLoading, isAuthenticated, isAuthConfigured, fetchTemplates]);
-
- const handleSaveTemplate = async (updatedTemplate: ContractTemplate) => {
- if (updatedTemplate.isBuiltIn) {
- // Built-in templates are read-only, just close the editor
- setEditingTemplate(null);
- return;
- }
-
- try {
- setSaving(true);
- setError(null);
-
- // Convert to API format
- const phases: PhaseDefinition[] = updatedTemplate.phases.map((p, index) => ({
- id: p.id,
- name: p.name,
- order: index,
- }));
-
- const deliverables: Record<string, DeliverableDefinition[]> = {};
- for (const phase of updatedTemplate.phases) {
- if (phase.deliverables.length > 0) {
- deliverables[phase.id] = phase.deliverables.map((d) => ({
- id: d.id,
- name: d.name,
- priority: "required" as const,
- }));
- }
- }
-
- await updateContractTemplate(updatedTemplate.id, {
- name: updatedTemplate.name,
- description: updatedTemplate.description,
- phases,
- defaultPhase: phases[0]?.id || "execute",
- deliverables: Object.keys(deliverables).length > 0 ? deliverables : undefined,
- });
-
- // Refresh templates from server
- await fetchTemplates();
- setEditingTemplate(null);
- } catch (err) {
- console.error("Failed to update template:", err);
- setError(err instanceof Error ? err.message : "Failed to update template");
- } finally {
- setSaving(false);
- }
- };
-
- const handleCreateTemplate = async () => {
- if (!newTemplateName.trim()) return;
-
- try {
- setSaving(true);
- setError(null);
-
- const phases: PhaseDefinition[] = [
- { id: "execute", name: "Execute", order: 0 },
- ];
-
- await createContractTemplate({
- name: newTemplateName.trim(),
- description: newTemplateDescription.trim() || "Custom contract template",
- phases,
- defaultPhase: "execute",
- });
-
- // Refresh templates from server
- await fetchTemplates();
-
- setNewTemplateName("");
- setNewTemplateDescription("");
- setShowNewTemplateForm(false);
- } catch (err) {
- console.error("Failed to create template:", err);
- setError(err instanceof Error ? err.message : "Failed to create template");
- } finally {
- setSaving(false);
- }
- };
-
- const handleDeleteTemplate = async (templateId: string) => {
- const template = templates.find((t) => t.id === templateId);
- if (template?.isBuiltIn) return;
-
- if (window.confirm(`Are you sure you want to delete "${template?.name}"?`)) {
- try {
- setSaving(true);
- setError(null);
- await deleteContractTemplate(templateId);
- await fetchTemplates();
- } catch (err) {
- console.error("Failed to delete template:", err);
- setError(err instanceof Error ? err.message : "Failed to delete template");
- } finally {
- setSaving(false);
- }
- }
- };
-
- const handleRefresh = () => {
- fetchTemplates();
- };
-
- // Show loading state
- if (authLoading || loading) {
- return (
- <div className="relative z-10 min-h-screen flex items-center justify-center bg-[#0a1628]">
- <div className="text-[#75aafc] font-mono text-sm animate-pulse">
- Loading...
- </div>
- </div>
- );
- }
-
- // Editor view
- if (editingTemplate) {
- return (
- <div className="relative z-10 min-h-screen bg-[#0a1628]">
- <Masthead />
- <main className="max-w-4xl mx-auto px-4 py-6">
- <TemplateEditor
- template={editingTemplate}
- onSave={handleSaveTemplate}
- onCancel={() => setEditingTemplate(null)}
- readOnly={editingTemplate.isBuiltIn}
- />
- </main>
- </div>
- );
- }
-
- return (
- <div className="relative z-10 min-h-screen bg-[#0a1628]">
- <Masthead />
- <main className="max-w-6xl mx-auto px-4 py-6">
- {/* Error display */}
- {error && (
- <div className="mb-4 p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs">
- {error}
- <button
- type="button"
- onClick={() => setError(null)}
- className="ml-2 text-red-400/70 hover:text-red-400"
- >
- Dismiss
- </button>
- </div>
- )}
-
- {/* Header */}
- <div className="flex justify-between items-start mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]">
- <div>
- <h1 className="text-lg font-mono uppercase tracking-wide text-[#9bc3ff] mb-1">
- Contract Templates
- </h1>
- <p className="text-xs font-mono text-[#75aafc] opacity-70">
- Manage contract types and their phase deliverables
- </p>
- </div>
- <div className="flex gap-3">
- <button
- type="button"
- onClick={handleRefresh}
- disabled={saving}
- className="px-3 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors disabled:opacity-50"
- >
- Refresh
- </button>
- <button
- type="button"
- onClick={() => setShowNewTemplateForm(true)}
- disabled={saving}
- className="px-3 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors disabled:opacity-50"
- >
- + New Template
- </button>
- </div>
- </div>
-
- {/* New Template Form */}
- {showNewTemplateForm && (
- <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-4 mb-6">
- <div className="flex gap-3 items-center">
- <input
- type="text"
- className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
- placeholder="Template name..."
- value={newTemplateName}
- onChange={(e) => setNewTemplateName(e.target.value)}
- disabled={saving}
- />
- <input
- type="text"
- className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
- placeholder="Description (optional)..."
- value={newTemplateDescription}
- onChange={(e) => setNewTemplateDescription(e.target.value)}
- disabled={saving}
- />
- <button
- type="button"
- onClick={handleCreateTemplate}
- disabled={saving || !newTemplateName.trim()}
- className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors disabled:opacity-50"
- >
- {saving ? "Creating..." : "Create"}
- </button>
- <button
- type="button"
- onClick={() => setShowNewTemplateForm(false)}
- disabled={saving}
- className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors disabled:opacity-50"
- >
- Cancel
- </button>
- </div>
- </div>
- )}
-
- {/* Templates Grid */}
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
- {templates.map((template) => (
- <div
- key={template.id}
- className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] hover:border-[rgba(117,170,252,0.45)] transition-colors"
- >
- {/* Card Header */}
- <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between">
- <h3 className="font-mono text-sm text-white">{template.name}</h3>
- {template.isBuiltIn ? (
- <span className="px-2 py-0.5 bg-[rgba(117,170,252,0.15)] text-[#75aafc] font-mono text-[10px] uppercase tracking-wide">
- Built-in
- </span>
- ) : (
- <span className="px-2 py-0.5 bg-[rgba(100,200,100,0.15)] text-[#7bc97b] font-mono text-[10px] uppercase tracking-wide">
- Custom
- </span>
- )}
- </div>
-
- {/* Card Body */}
- <div className="px-4 py-3">
- <p className="text-xs font-mono text-[#75aafc] opacity-70 mb-4 min-h-[2.5rem]">
- {template.description}
- </p>
-
- {/* Phases */}
- <div className="space-y-2 mb-4">
- {template.phases.map((phase, index) => (
- <div key={phase.id} className="flex items-start gap-2">
- <div className="flex flex-col items-center">
- <span className="w-2 h-2 rounded-full bg-[#3f6fb3]" />
- {index < template.phases.length - 1 && (
- <span className="w-px h-4 bg-[rgba(117,170,252,0.25)]" />
- )}
- </div>
- <div className="flex-1 min-w-0">
- <span className="text-xs font-mono text-[#9bc3ff]">
- {phase.name}
- </span>
- <span className="text-[10px] font-mono text-[#556677] ml-2">
- {phase.deliverables.length === 0
- ? "(no deliverables)"
- : phase.deliverables.map((d) => d.name).join(", ")}
- </span>
- </div>
- </div>
- ))}
- </div>
- </div>
-
- {/* Card Footer */}
- <div className="px-4 py-3 border-t border-[rgba(117,170,252,0.15)] flex gap-2">
- <button
- type="button"
- onClick={() => setEditingTemplate(template)}
- disabled={saving}
- className="flex-1 px-3 py-1.5 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors disabled:opacity-50"
- >
- {template.isBuiltIn ? "View" : "Edit"}
- </button>
- {!template.isBuiltIn && (
- <button
- type="button"
- onClick={() => handleDeleteTemplate(template.id)}
- disabled={saving}
- className="px-3 py-1.5 border border-[rgba(255,100,100,0.25)] text-[#ff6464] font-mono text-xs uppercase tracking-wide hover:border-[rgba(255,100,100,0.5)] hover:bg-[rgba(255,100,100,0.05)] transition-colors disabled:opacity-50"
- >
- Delete
- </button>
- )}
- </div>
- </div>
- ))}
- </div>
- </main>
- </div>
- );
-}
diff --git a/makima/frontend/src/types/templates.ts b/makima/frontend/src/types/templates.ts
deleted file mode 100644
index ca337c5..0000000
--- a/makima/frontend/src/types/templates.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-// Contract Template types
-export interface Deliverable {
- id: string;
- name: string;
-}
-
-export interface Phase {
- id: string;
- name: string;
- deliverables: Deliverable[];
-}
-
-export interface ContractTemplate {
- id: string;
- name: string;
- description: string;
- phases: Phase[];
- isBuiltIn: boolean;
-}
-
-// Default built-in templates
-// NOTE: Deliverable IDs must match backend phase_guidance.rs exactly
-export const DEFAULT_TEMPLATES: ContractTemplate[] = [
- {
- id: "simple",
- name: "Simple",
- description: "A simple contract with plan and execute phases.",
- isBuiltIn: true,
- phases: [
- {
- id: "plan",
- name: "Plan",
- deliverables: [{ id: "plan-document", name: "Plan" }],
- },
- {
- id: "execute",
- name: "Execute",
- deliverables: [{ id: "pull-request", name: "Pull Request" }],
- },
- ],
- },
- {
- id: "specification",
- name: "Specification",
- description:
- "A comprehensive contract with research, specification, planning, execution, and review phases.",
- isBuiltIn: true,
- phases: [
- {
- id: "research",
- name: "Research",
- deliverables: [{ id: "research-notes", name: "Research Notes" }],
- },
- {
- id: "specify",
- name: "Specify",
- deliverables: [{ id: "requirements-document", name: "Requirements Document" }],
- },
- {
- id: "plan",
- name: "Plan",
- deliverables: [{ id: "plan-document", name: "Plan" }],
- },
- {
- id: "execute",
- name: "Execute",
- deliverables: [{ id: "pull-request", name: "Pull Request" }],
- },
- {
- id: "review",
- name: "Review",
- deliverables: [{ id: "release-notes", name: "Release Notes" }],
- },
- ],
- },
- {
- id: "execute",
- name: "Execute",
- description:
- "A minimal contract with only an execute phase and no deliverables.",
- isBuiltIn: true,
- phases: [
- {
- id: "execute",
- name: "Execute",
- deliverables: [],
- },
- ],
- },
-];