diff options
| author | soryu <soryu@soryu.co> | 2026-01-24 20:06:28 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-24 20:06:28 +0000 |
| commit | 6364363d1418728351f252b799d397b756e1f985 (patch) | |
| tree | 9b5227f141bfc587b487265b3687a11f6f504be3 /makima/frontend | |
| parent | 792d12df6b1b1bc4f327cbe8e71e7986c67e98f6 (diff) | |
| download | soryu-6364363d1418728351f252b799d397b756e1f985.tar.gz soryu-6364363d1418728351f252b799d397b756e1f985.zip | |
feat: Simplify contract deliverables and add Templates UI
## Backend Changes
### Phase Deliverables Simplified
- **Simple contract type**:
- Plan phase: Only 'Plan' deliverable (required)
- Execute phase: Only 'PR' deliverable (required)
- **Specification contract type**:
- Research phase: Only 'Research Notes' deliverable (required)
- Specify phase: Only 'Requirements Document' deliverable (required)
- Plan phase: Only 'Plan' deliverable (required)
- Execute phase: Only 'PR' deliverable (required)
- Review phase: Only 'Release Notes' deliverable (required)
### New 'execute' Contract Type
- Only has 'execute' phase (no plan or review phases)
- NO deliverables at all - executes tasks directly
- Added to ContractType enum with proper Display/FromStr implementations
- Added helper methods: `initial_phase()`, `terminal_phase()`
### API Updates
- Added `get_phase_deliverables_for_type()` for contract-type-aware deliverables
- Added `get_phase_checklist_for_type()` for contract-type-aware checklists
- Added `check_phase_completion_for_type()` for contract-type-aware completion checks
- Added `check_deliverables_met()` function for deliverable validation
- Added `should_auto_progress()` for autonomous contract progression
- Added new ContractToolRequest::CheckDeliverablesMet tool
## Frontend Changes (makima/frontend)
### Templates Page
- Add TemplateEditor component for editing phase deliverables
- Create Templates page with template card grid layout
- Add navigation link in NavStrip
- Implement three built-in templates: Simple, Specification, Execute
- Support for creating custom templates with configurable phases/deliverables
- Templates are persisted to localStorage
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/components/templates/TemplateEditor.tsx | 248 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 9 | ||||
| -rw-r--r-- | makima/frontend/src/routes/templates.tsx | 268 | ||||
| -rw-r--r-- | makima/frontend/src/types/templates.ts | 89 |
5 files changed, 615 insertions, 0 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 7e12c75..2838469 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -15,6 +15,7 @@ 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/templates/TemplateEditor.tsx b/makima/frontend/src/components/templates/TemplateEditor.tsx new file mode 100644 index 0000000..03382f3 --- /dev/null +++ b/makima/frontend/src/components/templates/TemplateEditor.tsx @@ -0,0 +1,248 @@ +import { useState } from "react"; +import type { ContractTemplate, Phase, Deliverable } from "../../types/templates"; + +interface Props { + template: ContractTemplate; + onSave: (template: ContractTemplate) => void; + onCancel: () => void; +} + +export function TemplateEditor({ template, onSave, onCancel }: 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"> + Edit Template: {template.name} + </h2> + <p className="text-xs font-mono text-[#75aafc] opacity-70"> + {template.description} + </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]" + value={phase.name} + onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)} + placeholder="Phase name" + /> + {!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" + > + Cancel + </button> + <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/main.tsx b/makima/frontend/src/main.tsx index 19f02d1..0464495 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -17,6 +17,7 @@ import MeshPage from "./routes/mesh"; import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; +import TemplatesPage from "./routes/templates"; createRoot(document.getElementById("root")!).render( <StrictMode> @@ -117,6 +118,14 @@ createRoot(document.getElementById("root")!).render( </ProtectedRoute> } /> + <Route + path="/templates" + element={ + <ProtectedRoute> + <TemplatesPage /> + </ProtectedRoute> + } + /> </Routes> </BrowserRouter> </SupervisorQuestionsProvider> diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx new file mode 100644 index 0000000..ce944a8 --- /dev/null +++ b/makima/frontend/src/routes/templates.tsx @@ -0,0 +1,268 @@ +import { useState, useEffect } 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"; + +const STORAGE_KEY = "makima_contract_templates"; + +export default function TemplatesPage() { + const navigate = useNavigate(); + const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); + + const [templates, setTemplates] = useState<ContractTemplate[]>(() => { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + return JSON.parse(saved); + } catch { + return DEFAULT_TEMPLATES; + } + } + return DEFAULT_TEMPLATES; + }); + + 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]); + + const saveTemplates = (newTemplates: ContractTemplate[]) => { + setTemplates(newTemplates); + localStorage.setItem(STORAGE_KEY, JSON.stringify(newTemplates)); + }; + + const handleSaveTemplate = (updatedTemplate: ContractTemplate) => { + const newTemplates = templates.map((t) => + t.id === updatedTemplate.id ? updatedTemplate : t + ); + saveTemplates(newTemplates); + setEditingTemplate(null); + }; + + const handleCreateTemplate = () => { + if (!newTemplateName.trim()) return; + + const newTemplate: ContractTemplate = { + id: `custom-${Date.now()}`, + name: newTemplateName.trim(), + description: newTemplateDescription.trim() || "Custom contract template", + isBuiltIn: false, + phases: [ + { + id: `phase-${Date.now()}`, + name: "Execute", + deliverables: [], + }, + ], + }; + + saveTemplates([...templates, newTemplate]); + setNewTemplateName(""); + setNewTemplateDescription(""); + setShowNewTemplateForm(false); + }; + + const handleDeleteTemplate = (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}"?`)) { + saveTemplates(templates.filter((t) => t.id !== templateId)); + } + }; + + const handleResetToDefaults = () => { + if ( + window.confirm( + "Reset all templates to defaults? This will remove any custom templates." + ) + ) { + saveTemplates(DEFAULT_TEMPLATES); + } + }; + + // Show loading state + if (authLoading) { + return ( + <div className="min-h-screen flex items-center justify-center"> + <div className="text-[#75aafc] font-mono text-sm animate-pulse"> + Loading... + </div> + </div> + ); + } + + // Editor view + if (editingTemplate) { + return ( + <div className="min-h-screen"> + <Masthead /> + <main className="max-w-4xl mx-auto px-4 py-6"> + <TemplateEditor + template={editingTemplate} + onSave={handleSaveTemplate} + onCancel={() => setEditingTemplate(null)} + /> + </main> + </div> + ); + } + + return ( + <div className="min-h-screen"> + <Masthead /> + <main className="max-w-6xl mx-auto px-4 py-6"> + {/* 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={handleResetToDefaults} + 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" + > + Reset to Defaults + </button> + <button + type="button" + onClick={() => setShowNewTemplateForm(true)} + 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" + > + + 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)} + /> + <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)} + /> + <button + type="button" + onClick={handleCreateTemplate} + 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" + > + Create + </button> + <button + type="button" + onClick={() => setShowNewTemplateForm(false)} + 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" + > + 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)} + 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" + > + Edit + </button> + {!template.isBuiltIn && ( + <button + type="button" + onClick={() => handleDeleteTemplate(template.id)} + 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" + > + Delete + </button> + )} + </div> + </div> + ))} + </div> + </main> + </div> + ); +} diff --git a/makima/frontend/src/types/templates.ts b/makima/frontend/src/types/templates.ts new file mode 100644 index 0000000..77ba89e --- /dev/null +++ b/makima/frontend/src/types/templates.ts @@ -0,0 +1,89 @@ +// 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 +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-deliverable", name: "Plan" }], + }, + { + id: "execute", + name: "Execute", + deliverables: [{ id: "pr-deliverable", name: "PR" }], + }, + ], + }, + { + 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", name: "Requirements Document" }], + }, + { + id: "plan", + name: "Plan", + deliverables: [{ id: "plan-deliverable", name: "Plan" }], + }, + { + id: "execute", + name: "Execute", + deliverables: [{ id: "pr-deliverable", name: "PR" }], + }, + { + 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: [], + }, + ], + }, +]; |
