feat: create new project added

This commit is contained in:
thanhnv
2026-07-11 22:42:42 +09:00
parent 193a449829
commit 159022c73f
11 changed files with 405 additions and 28 deletions
@@ -46,6 +46,9 @@ function WorkerCard({ title, subtitle, status, detail, provider, model }: {
export function Goals() {
const [goal, setGoal] = useState('');
const [projectId, setProjectId] = useState('');
const [creatingProject, setCreatingProject] = useState(false);
const [newProjectId, setNewProjectId] = useState('');
const [newProjectDomain, setNewProjectDomain] = useState('');
const [actor] = useState(DEFAULT_ACTOR);
const [searchParams, setSearchParams] = useSearchParams();
const selectedId = searchParams.get('id') ?? '';
@@ -73,6 +76,16 @@ export function Goals() {
void queryClient.invalidateQueries({ queryKey: ['goals'] });
},
});
const createProject = useMutation({
mutationFn: () => api.createGoalProject(actor, { projectId: newProjectId.trim(), domain: newProjectDomain.trim() }),
onSuccess: (project) => {
setProjectId(project.project_id);
setNewProjectId('');
setNewProjectDomain('');
setCreatingProject(false);
void queryClient.invalidateQueries({ queryKey: ['goal-projects'] });
},
});
const selected = selectedQuery.data;
const localStage = selected?.stages.find((stage) => stage.id === 'local-worker');
@@ -89,15 +102,32 @@ export function Goals() {
</section>
<Card title="Give CASAN an objective">
<label className="mb-4 block">
<span className="text-sm font-semibold text-slate-800">Project workspace</span>
<div className="mb-4">
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-semibold text-slate-800">Project workspace</span>
<button type="button" onClick={() => setCreatingProject((value) => !value)} className="rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-xs font-semibold text-indigo-700 transition hover:border-indigo-300 hover:bg-indigo-100 focus:outline-none focus:ring-2 focus:ring-indigo-400">
{creatingProject ? 'Cancel' : '+ New project'}
</button>
</div>
<span className="mt-1 block text-xs leading-5 text-slate-500">Only server-registered, allowlisted roots are available. Models receive a bounded redacted snapshot—not filesystem access.</span>
<select value={effectiveProject} onChange={(event) => setProjectId(event.target.value)} disabled={projectsQuery.isLoading || projects.length === 0} className="mt-2 w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm outline-none transition focus:border-indigo-400 focus:ring-4 focus:ring-indigo-100 disabled:bg-slate-100">
{projects.map((project) => <option key={project.project_id} value={project.project_id}>{project.domain} · {project.project_id}</option>)}
</select>
{projectsQuery.isError && <span role="alert" className="mt-2 block text-xs font-medium text-rose-700">Could not load the allowlisted project registry.</span>}
{effectiveProject && <span className="mt-2 block font-mono text-[11px] text-slate-400">Context roots: {projects.find((project) => project.project_id === effectiveProject)?.context_roots.join(', ')}</span>}
</label>
{creatingProject && (
<div className="mt-4 rounded-2xl border border-indigo-200 bg-indigo-50/60 p-4">
<div className="text-sm font-semibold text-indigo-950">Register a new governed workspace</div>
<p className="mt-1 text-xs leading-5 text-indigo-800/75">CASAN creates an empty directory under <span className="font-mono">apps/projects/&lt;project-id&gt;</span>. Absolute paths and external roots are never accepted.</p>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<label><span className="text-xs font-semibold text-slate-700">Project ID</span><input value={newProjectId} onChange={(event) => setNewProjectId(event.target.value)} placeholder="customer-portal" maxLength={64} className="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2.5 text-sm outline-none transition focus:border-indigo-400 focus:ring-4 focus:ring-indigo-100" /></label>
<label><span className="text-xs font-semibold text-slate-700">Display name</span><input value={newProjectDomain} onChange={(event) => setNewProjectDomain(event.target.value)} placeholder="Customer Portal" maxLength={100} className="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2.5 text-sm outline-none transition focus:border-indigo-400 focus:ring-4 focus:ring-indigo-100" /></label>
</div>
<div className="mt-4 flex justify-end"><button type="button" disabled={!/^[A-Za-z][A-Za-z0-9._-]{2,63}$/.test(newProjectId.trim()) || newProjectDomain.trim().length < 3 || createProject.isPending} onClick={() => createProject.mutate()} className="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-indigo-700 disabled:cursor-not-allowed disabled:bg-slate-300">{createProject.isPending ? 'Creating…' : 'Create and select'}</button></div>
{createProject.isError && <div role="alert" className="mt-3 rounded-xl border border-rose-200 bg-rose-50 p-3 text-xs font-medium text-rose-700">{errorMessage(createProject.error)}</div>}
</div>
)}
</div>
<textarea
value={goal}
onChange={(event) => setGoal(event.target.value)}
@@ -143,6 +173,7 @@ export function Goals() {
{selected.error && <div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700">{selected.error}</div>}
{selected.approval && <div className="mt-4 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900"><div className="font-semibold">Workspace side effect withheld</div><p className="mt-1 leading-6">Proposal <span className="font-mono">{selected.approval.id}</span> is {selected.approval.status}. No source file or runtime action was changed.</p><Link to="/approvals" className="mt-3 inline-flex rounded-lg bg-amber-700 px-3 py-2 text-xs font-semibold text-white hover:bg-amber-800 focus:outline-none focus:ring-2 focus:ring-amber-500">Open Approvals</Link></div>}
{selected.local_draft && <details className="mt-5 rounded-xl border border-slate-200 p-4"><summary className="cursor-pointer text-sm font-semibold text-slate-700">Inspect local worker draft</summary><div className="mt-4"><MarkdownText text={selected.local_draft} /></div></details>}
{selected.reviewer_attempts && selected.reviewer_attempts.length > 0 && <details className="mt-5 rounded-xl border border-slate-200 p-4"><summary className="cursor-pointer text-sm font-semibold text-slate-700">Fallback attempt ledger ({selected.reviewer_attempts.length})</summary><div className="mt-4 space-y-2">{selected.reviewer_attempts.map((attempt) => <div key={attempt.attempt} className="flex flex-wrap items-center justify-between gap-2 rounded-lg bg-slate-50 px-3 py-2 text-xs"><span className="font-semibold text-slate-700">{attempt.attempt}. reviewer</span><span className="font-mono text-slate-500">{attempt.provider} · {attempt.model}</span><StatusBadge value={attempt.status} /></div>)}</div></details>}
</Card>
<TraceExplorer traceId={selected.trace_id} />