From cf0a25af1d2834bfe6c5ea892ce5769936e5a673 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 3 Feb 2026 22:01:29 +0000 Subject: Add makima chain mechanism --- makima/frontend/src/routes/chains.tsx | 283 ++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 makima/frontend/src/routes/chains.tsx (limited to 'makima/frontend/src/routes/chains.tsx') diff --git a/makima/frontend/src/routes/chains.tsx b/makima/frontend/src/routes/chains.tsx new file mode 100644 index 0000000..f01d5a6 --- /dev/null +++ b/makima/frontend/src/routes/chains.tsx @@ -0,0 +1,283 @@ +import { useState, useCallback, useEffect } from "react"; +import { useParams, useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { ChainList } from "../components/chains/ChainList"; +import { ChainEditor } from "../components/chains/ChainEditor"; +import { useChains } from "../hooks/useChains"; +import { useAuth } from "../contexts/AuthContext"; +import type { + ChainSummary, + ChainWithContracts, + ChainGraphResponse, + CreateChainRequest, +} from "../lib/api"; + +export default function ChainsPage() { + const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); + const navigate = useNavigate(); + + // Redirect to login if not authenticated (when auth is configured) + useEffect(() => { + if (!authLoading && isAuthConfigured && !isAuthenticated) { + navigate("/login"); + } + }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); + + // Show loading while checking auth + if (authLoading) { + return ( +
+ +
+

Loading...

+
+
+ ); + } + + // Don't render if not authenticated (will redirect) + if (isAuthConfigured && !isAuthenticated) { + return null; + } + + return ; +} + +function ChainsPageContent() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { + chains, + loading, + error, + createNewChain, + archiveExistingChain, + getChainById, + getGraph, + } = useChains(); + + const [chainDetail, setChainDetail] = useState(null); + const [chainGraph, setChainGraph] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + const [isCreating, setIsCreating] = useState(false); + + // Load chain detail when ID changes + useEffect(() => { + if (id) { + setDetailLoading(true); + Promise.all([getChainById(id), getGraph(id)]).then(([chain, graph]) => { + setChainDetail(chain); + setChainGraph(graph); + setDetailLoading(false); + }); + } else { + setChainDetail(null); + setChainGraph(null); + } + }, [id, getChainById, getGraph]); + + const handleSelect = useCallback( + (chainId: string) => { + navigate(`/chains/${chainId}`); + }, + [navigate] + ); + + const handleBack = useCallback(() => { + navigate("/chains"); + }, [navigate]); + + const handleCreate = useCallback(() => { + setIsCreating(true); + }, []); + + const handleCreateSubmit = useCallback( + async (name: string, description: string) => { + const data: CreateChainRequest = { + name: name.trim(), + description: description.trim() || undefined, + }; + + try { + const result = await createNewChain(data); + if (result) { + setIsCreating(false); + navigate(`/chains/${result.chain.id}`); + } + } catch (err) { + console.error("Failed to create chain:", err); + } + }, + [createNewChain, navigate] + ); + + const handleCreateCancel = useCallback(() => { + setIsCreating(false); + }, []); + + const handleArchive = useCallback( + async (chain: ChainSummary) => { + if (confirm(`Are you sure you want to archive "${chain.name}"?`)) { + const success = await archiveExistingChain(chain.id); + if (success && chain.id === id) { + navigate("/chains"); + } + } + }, + [archiveExistingChain, id, navigate] + ); + + const handleRefresh = useCallback(async () => { + if (id) { + const [chain, graph] = await Promise.all([getChainById(id), getGraph(id)]); + setChainDetail(chain); + setChainGraph(graph); + } + }, [id, getChainById, getGraph]); + + const handleContractClick = useCallback( + (contractId: string) => { + navigate(`/contracts/${contractId}`); + }, + [navigate] + ); + + return ( +
+ +
+ {error && ( +
+ {error} +
+ )} + + {/* Create chain modal */} + {isCreating && ( + + )} + +
+ {/* Chain list */} + + + {/* Chain detail/editor or empty state */} + {chainDetail ? ( + + ) : ( +
+
+

+ Select a chain or create a new one +

+ +
+
+ )} +
+
+
+ ); +} + +interface CreateChainModalProps { + onSubmit: (name: string, description: string) => void; + onCancel: () => void; +} + +function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + const handleSubmit = () => { + if (name.trim()) { + onSubmit(name.trim(), description.trim()); + } + }; + + return ( +
+
+

+ Create Chain +

+ +
+ {/* Chain name */} +
+ + setName(e.target.value)} + placeholder="e.g., Feature Implementation" + className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" + autoFocus + /> +
+ + {/* Description */} +
+ +