summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-05 11:10:23 +0000
committersoryu <soryu@soryu.co>2026-02-05 11:10:23 +0000
commitf1a50b80f3969d150bd1c31edde0aff05369157e (patch)
treeeef4a1e8ba4012d5ee67cd5dd01d3a7380f215ec /makima/frontend/src/routes
parent5205db1f26cff0b59c567915966ed1dd892ab472 (diff)
downloadsoryu-f1a50b80f3969d150bd1c31edde0aff05369157e.tar.gz
soryu-f1a50b80f3969d150bd1c31edde0aff05369157e.zip
Add repository selection to chain creation modal
- Update CreateChainModal to include repository input fields - Add repository suggestions from history using getRepositorySuggestions - Support both remote (URL) and local (path) repositories - First repository added becomes primary automatically - Pass repositories to CreateChainRequest Also includes backend changes: - Copy chain repositories to contracts when created from definitions - Add created_at field to ChainContractDetail Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/chains.tsx231
1 files changed, 222 insertions, 9 deletions
diff --git a/makima/frontend/src/routes/chains.tsx b/makima/frontend/src/routes/chains.tsx
index 23484b4..9b33304 100644
--- a/makima/frontend/src/routes/chains.tsx
+++ b/makima/frontend/src/routes/chains.tsx
@@ -10,7 +10,10 @@ import type {
ChainWithContracts,
ChainGraphResponse,
CreateChainRequest,
+ AddChainRepositoryRequest,
+ RepositoryHistoryEntry,
} from "../lib/api";
+import { getRepositorySuggestions } from "../lib/api";
export default function ChainsPage() {
const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
@@ -92,10 +95,11 @@ function ChainsPageContent() {
}, []);
const handleCreateSubmit = useCallback(
- async (name: string, description: string) => {
+ async (name: string, description: string, repositories: AddChainRepositoryRequest[]) => {
const data: CreateChainRequest = {
name: name.trim(),
description: description.trim() || undefined,
+ repositories: repositories.length > 0 ? repositories : undefined,
};
try {
@@ -203,23 +207,89 @@ function ChainsPageContent() {
}
interface CreateChainModalProps {
- onSubmit: (name: string, description: string) => void;
+ 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<AddChainRepositoryRequest[]>([]);
+
+ // Repository input state
+ const [repoMode, setRepoMode] = useState<RepoMode>(null);
+ const [repoName, setRepoName] = useState("");
+ const [repoUrl, setRepoUrl] = useState("");
+ const [repoPath, setRepoPath] = useState("");
+
+ // Suggestions
+ const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]);
+ 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());
+ onSubmit(name.trim(), description.trim(), repositories);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
- <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]">
+ <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto">
<h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
Create Chain
</h3>
@@ -228,7 +298,7 @@ function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) {
{/* Chain name */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Chain Name
+ Chain Name *
</label>
<input
type="text"
@@ -249,15 +319,158 @@ function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) {
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Describe what this chain accomplishes..."
- rows={3}
+ rows={2}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
/>
</div>
+ {/* Repositories */}
+ <div>
+ <label className="block font-mono text-xs text-[#8b949e] uppercase mb-2">
+ Repositories
+ </label>
+
+ {/* Added repositories */}
+ {repositories.length > 0 && (
+ <div className="space-y-2 mb-3">
+ {repositories.map((repo, index) => (
+ <div
+ key={index}
+ className="flex items-center gap-2 px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)]"
+ >
+ <span className="font-mono text-[10px] text-[#556677] uppercase">
+ {repo.sourceType === "remote" ? "URL" : "Local"}
+ </span>
+ <span className="font-mono text-xs text-[#dbe7ff] flex-1 truncate">
+ {repo.name}
+ </span>
+ {repo.isPrimary && (
+ <span className="font-mono text-[8px] text-[#75aafc] uppercase px-1 border border-[#75aafc]/30">
+ primary
+ </span>
+ )}
+ <button
+ onClick={() => handleRemoveRepo(index)}
+ className="font-mono text-xs text-[#556677] hover:text-red-400"
+ >
+ ✕
+ </button>
+ </div>
+ ))}
+ </div>
+ )}
+
+ {/* Add repository form */}
+ {repoMode ? (
+ <div className="p-3 border border-[rgba(117,170,252,0.3)] space-y-3">
+ <div className="flex items-center justify-between">
+ <span className="font-mono text-[10px] text-[#75aafc] uppercase">
+ Add {repoMode === "remote" ? "Remote" : "Local"} Repository
+ </span>
+ {suggestions.length > 0 && (
+ <button
+ onClick={() => setShowSuggestions(!showSuggestions)}
+ className="font-mono text-[10px] text-[#556677] hover:text-[#9bc3ff]"
+ >
+ {showSuggestions ? "Hide" : `${suggestions.length} suggestions`}
+ </button>
+ )}
+ </div>
+
+ {/* Suggestions dropdown */}
+ {showSuggestions && suggestions.length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-28 overflow-y-auto">
+ {suggestions.map((s) => (
+ <button
+ key={s.id}
+ onClick={() => applySuggestion(s)}
+ className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
+ >
+ <div className="flex items-center justify-between">
+ <span className="text-[#9bc3ff] truncate">{s.name}</span>
+ <span className="text-[10px] text-[#556677]">{s.useCount}×</span>
+ </div>
+ <div className="text-[10px] text-[#556677] truncate">
+ {repoMode === "local" ? s.localPath : s.repositoryUrl}
+ </div>
+ </button>
+ ))}
+ </div>
+ )}
+
+ <input
+ type="text"
+ value={repoName}
+ onChange={(e) => setRepoName(e.target.value)}
+ placeholder="Repository name"
+ className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
+ />
+
+ {repoMode === "remote" ? (
+ <input
+ type="text"
+ value={repoUrl}
+ onChange={(e) => setRepoUrl(e.target.value)}
+ placeholder="https://github.com/owner/repo"
+ className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
+ />
+ ) : (
+ <input
+ type="text"
+ value={repoPath}
+ onChange={(e) => setRepoPath(e.target.value)}
+ placeholder="/path/to/repository"
+ className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
+ />
+ )}
+
+ <div className="flex gap-2">
+ <button
+ onClick={() => setRepoMode(null)}
+ className="px-3 py-1.5 font-mono text-xs text-[#556677] hover:text-[#9bc3ff]"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleAddRepo}
+ disabled={
+ !repoName.trim() ||
+ (repoMode === "remote" && !repoUrl.trim()) ||
+ (repoMode === "local" && !repoPath.trim())
+ }
+ className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] disabled:opacity-50"
+ >
+ Add
+ </button>
+ </div>
+ </div>
+ ) : (
+ <div className="flex gap-2">
+ <button
+ onClick={() => setRepoMode("remote")}
+ className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]"
+ >
+ + Remote
+ </button>
+ <button
+ onClick={() => setRepoMode("local")}
+ className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]"
+ >
+ + Local
+ </button>
+ </div>
+ )}
+
+ {repositories.length === 0 && !repoMode && (
+ <p className="font-mono text-[10px] text-[#556677] mt-2">
+ Add repositories that contracts in this chain will work with
+ </p>
+ )}
+ </div>
+
<p className="font-mono text-xs text-[#8b949e]">
- A chain links multiple contracts together in a directed acyclic graph (DAG).
- Contracts can depend on each other, and dependent contracts start automatically
- when their dependencies complete.
+ A chain links multiple contracts together in a DAG. Contracts depend on each
+ other and start automatically when dependencies complete.
</p>
{/* Actions */}