From f19acd400cc5bbe1fe51c004c50ee90d704240d8 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 29 Jan 2026 02:56:44 +0000 Subject: Fix contract type selection --- makima/frontend/src/routes/contracts.tsx | 46 +----- makima/frontend/src/routes/templates.tsx | 248 +++++++++++++++++++++++-------- 2 files changed, 192 insertions(+), 102 deletions(-) (limited to 'makima/frontend/src/routes') diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index bb66215..fc76f60 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -96,49 +96,19 @@ function ContractsPageContent() { const [redTeamEnabled, setRedTeamEnabled] = useState(false); const [redTeamPrompt, setRedTeamPrompt] = useState(""); - // Fetch contract types when modal opens - merges built-in types with user templates + // Fetch contract types when modal opens - API returns both built-in and custom templates useEffect(() => { if (isCreating) { setContractTypesLoading(true); - // Load user templates from localStorage - const loadUserTemplates = (): ContractTypeTemplate[] => { - try { - const saved = localStorage.getItem("makima_contract_templates"); - if (saved) { - const templates = JSON.parse(saved); - // Convert user templates to ContractTypeTemplate format, excluding built-ins - return templates - .filter((t: { isBuiltIn?: boolean }) => !t.isBuiltIn) - .map((t: { id: string; name: string; description: string; phases: { id: string; name: string }[] }) => ({ - id: t.id, - name: t.name, - description: t.description, - phases: t.phases.map((p: { id: string }) => p.id) as ContractPhase[], - phaseNames: Object.fromEntries(t.phases.map((p: { id: string; name: string }) => [p.id, p.name])), - defaultPhase: (t.phases[0]?.id || "execute") as ContractPhase, - isBuiltin: false, - })); - } - } catch { - // Ignore localStorage errors - } - return []; - }; - listContractTypes() .then((res) => { - // Merge built-in types from API with user templates from localStorage - const userTemplates = loadUserTemplates(); - // Filter out any user templates that have the same ID as built-in types - const builtinIds = new Set(res.contractTypes.map(t => t.id)); - const uniqueUserTemplates = userTemplates.filter(t => !builtinIds.has(t.id)); - setContractTypes([...res.contractTypes, ...uniqueUserTemplates]); + setContractTypes(res.contractTypes); setContractTypesLoading(false); }) .catch((err) => { console.error("Failed to fetch contract types:", err); - // Fall back to built-in types + user templates + // Fall back to built-in types const builtinTypes: ContractTypeTemplate[] = [ { id: "simple", @@ -165,10 +135,7 @@ function ContractsPageContent() { isBuiltin: true, }, ]; - const userTemplates = loadUserTemplates(); - const builtinIds = new Set(builtinTypes.map(t => t.id)); - const uniqueUserTemplates = userTemplates.filter(t => !builtinIds.has(t.id)); - setContractTypes([...builtinTypes, ...uniqueUserTemplates]); + setContractTypes(builtinTypes); setContractTypesLoading(false); }); } @@ -261,11 +228,14 @@ function ContractsPageContent() { // Get default phase from contract types or fall back to static function const selectedType = contractTypes.find((t) => t.id === contractType); const defaultPhaseForType = selectedType?.defaultPhase || (contractType === "simple" ? "plan" : "research"); + const isCustomTemplate = selectedType && !selectedType.isBuiltin; const data: CreateContractRequest = { name: newContractName.trim(), description: newContractDescription.trim() || undefined, - contractType: contractType, + // For custom templates, send templateId instead of contractType + contractType: isCustomTemplate ? undefined : contractType, + templateId: isCustomTemplate ? contractType : undefined, initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined, localOnly: localOnly || undefined, redTeamEnabled: redTeamEnabled || undefined, diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx index 15bf95c..b2c9974 100644 --- a/makima/frontend/src/routes/templates.tsx +++ b/makima/frontend/src/routes/templates.tsx @@ -1,28 +1,27 @@ -import { useState, useEffect } from "react"; +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"; - -const STORAGE_KEY = "makima_contract_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(() => { - const saved = localStorage.getItem(STORAGE_KEY); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return DEFAULT_TEMPLATES; - } - } - return DEFAULT_TEMPLATES; - }); + const [templates, setTemplates] = useState(DEFAULT_TEMPLATES); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); const [editingTemplate, setEditingTemplate] = useState( null @@ -38,63 +37,161 @@ export default function TemplatesPage() { } }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - const saveTemplates = (newTemplates: ContractTemplate[]) => { - setTemplates(newTemplates); - localStorage.setItem(STORAGE_KEY, JSON.stringify(newTemplates)); - }; + // Fetch templates from API + const fetchTemplates = useCallback(async () => { + try { + setLoading(true); + setError(null); + const response = await listContractTypes(); - const handleSaveTemplate = (updatedTemplate: ContractTemplate) => { - const newTemplates = templates.map((t) => - t.id === updatedTemplate.id ? updatedTemplate : t - ); - saveTemplates(newTemplates); - setEditingTemplate(null); + // 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 = {}; + 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 = () => { + const handleCreateTemplate = async () => { 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); + 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 = (templateId: string) => { + 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}"?`)) { - saveTemplates(templates.filter((t) => t.id !== templateId)); + 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 handleResetToDefaults = () => { - if ( - window.confirm( - "Reset all templates to defaults? This will remove any custom templates." - ) - ) { - saveTemplates(DEFAULT_TEMPLATES); - } + const handleRefresh = () => { + fetchTemplates(); }; // Show loading state - if (authLoading) { + if (authLoading || loading) { return (
@@ -114,6 +211,7 @@ export default function TemplatesPage() { template={editingTemplate} onSave={handleSaveTemplate} onCancel={() => setEditingTemplate(null)} + readOnly={editingTemplate.isBuiltIn} />
@@ -124,6 +222,20 @@ export default function TemplatesPage() {
+ {/* Error display */} + {error && ( +
+ {error} + +
+ )} + {/* Header */}
@@ -137,15 +249,17 @@ export default function TemplatesPage() {
@@ -162,6 +276,7 @@ export default function TemplatesPage() { placeholder="Template name..." value={newTemplateName} onChange={(e) => setNewTemplateName(e.target.value)} + disabled={saving} /> setNewTemplateDescription(e.target.value)} + disabled={saving} /> @@ -245,15 +363,17 @@ export default function TemplatesPage() { {!template.isBuiltIn && ( -- cgit v1.2.3