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, AddChainRepositoryRequest, RepositoryHistoryEntry, } from "../lib/api"; import { getRepositorySuggestions } 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, repositories: AddChainRepositoryRequest[]) => { const data: CreateChainRequest = { name: name.trim(), description: description.trim() || undefined, repositories: repositories.length > 0 ? repositories : undefined, }; try { const result = await createNewChain(data); if (result) { setIsCreating(false); navigate(`/chains/${result.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, repositories: AddChainRepositoryRequest[]) => void; onCancel: () => void; } type RepoMode = "remote" | "local" | null; function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [repositories, setRepositories] = useState([]); // Repository input state const [repoMode, setRepoMode] = useState(null); const [repoName, setRepoName] = useState(""); const [repoUrl, setRepoUrl] = useState(""); const [repoPath, setRepoPath] = useState(""); // Suggestions const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); // Load suggestions when mode changes useEffect(() => { if (repoMode) { getRepositorySuggestions(repoMode, undefined, 10) .then((res) => { setSuggestions(res.entries); setShowSuggestions(res.entries.length > 0); }) .catch(() => { setSuggestions([]); setShowSuggestions(false); }); } else { setSuggestions([]); setShowSuggestions(false); } }, [repoMode]); const applySuggestion = (suggestion: RepositoryHistoryEntry) => { setRepoName(suggestion.name); if (suggestion.repositoryUrl) setRepoUrl(suggestion.repositoryUrl); if (suggestion.localPath) setRepoPath(suggestion.localPath); setShowSuggestions(false); }; const handleAddRepo = () => { if (!repoName.trim()) return; if (repoMode === "remote" && !repoUrl.trim()) return; if (repoMode === "local" && !repoPath.trim()) return; const newRepo: AddChainRepositoryRequest = { name: repoName.trim(), sourceType: repoMode || "remote", isPrimary: repositories.length === 0, // First one is primary ...(repoMode === "remote" ? { repositoryUrl: repoUrl.trim() } : { localPath: repoPath.trim() }), }; setRepositories([...repositories, newRepo]); setRepoMode(null); setRepoName(""); setRepoUrl(""); setRepoPath(""); }; const handleRemoveRepo = (index: number) => { const newRepos = repositories.filter((_, i) => i !== index); // If we removed the primary, make the first one primary if (newRepos.length > 0 && repositories[index]?.isPrimary) { newRepos[0].isPrimary = true; } setRepositories(newRepos); }; const handleSubmit = () => { if (name.trim()) { onSubmit(name.trim(), description.trim(), repositories); } }; 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 */}