diff options
| author | soryu <soryu@soryu.co> | 2026-01-29 02:56:44 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-29 02:56:44 +0000 |
| commit | f19acd400cc5bbe1fe51c004c50ee90d704240d8 (patch) | |
| tree | b7dcfd6926efcafd6eac33e713ebd321ec4284d0 /makima/frontend/src/routes | |
| parent | 7af8561677cfdcfd23d099a25783c7fef51d1ba6 (diff) | |
| download | soryu-f19acd400cc5bbe1fe51c004c50ee90d704240d8.tar.gz soryu-f19acd400cc5bbe1fe51c004c50ee90d704240d8.zip | |
Fix contract type selection
Diffstat (limited to 'makima/frontend/src/routes')
| -rw-r--r-- | makima/frontend/src/routes/contracts.tsx | 46 | ||||
| -rw-r--r-- | makima/frontend/src/routes/templates.tsx | 248 |
2 files changed, 192 insertions, 102 deletions
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<ContractTemplate[]>(() => { - const saved = localStorage.getItem(STORAGE_KEY); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return DEFAULT_TEMPLATES; - } - } - return DEFAULT_TEMPLATES; - }); + 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 @@ -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<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 = () => { + 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 ( <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"> @@ -114,6 +211,7 @@ export default function TemplatesPage() { template={editingTemplate} onSave={handleSaveTemplate} onCancel={() => setEditingTemplate(null)} + readOnly={editingTemplate.isBuiltIn} /> </main> </div> @@ -124,6 +222,20 @@ export default function TemplatesPage() { <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> @@ -137,15 +249,17 @@ export default function TemplatesPage() { <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" + 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" > - Reset to Defaults + Refresh </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" + 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> @@ -162,6 +276,7 @@ export default function TemplatesPage() { placeholder="Template name..." value={newTemplateName} onChange={(e) => setNewTemplateName(e.target.value)} + disabled={saving} /> <input type="text" @@ -169,18 +284,21 @@ export default function TemplatesPage() { placeholder="Description (optional)..." value={newTemplateDescription} onChange={(e) => setNewTemplateDescription(e.target.value)} + disabled={saving} /> <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" + 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" > - Create + {saving ? "Creating..." : "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" + 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> @@ -245,15 +363,17 @@ export default function TemplatesPage() { <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" + 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" > - Edit + {template.isBuiltIn ? "View" : "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" + 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> |
