|
@@ -22,7 +22,6 @@ import { createTeam, createTeamRun, createTeamVersion, listTeamRuns, listTeamVer
|
|
|
import { ApiErrorState } from "@/components/shared/ApiErrorState";
|
|
import { ApiErrorState } from "@/components/shared/ApiErrorState";
|
|
|
import { EmptyState } from "@/components/shared/EmptyState";
|
|
import { EmptyState } from "@/components/shared/EmptyState";
|
|
|
import { EntityListItem } from "@/components/shared/EntityListItem";
|
|
import { EntityListItem } from "@/components/shared/EntityListItem";
|
|
|
-import { JsonViewer } from "@/components/shared/JsonViewer";
|
|
|
|
|
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
|
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
|
|
import { MetricCard } from "@/components/shared/MetricCard";
|
|
import { MetricCard } from "@/components/shared/MetricCard";
|
|
|
import { PageHeader } from "@/components/shared/PageHeader";
|
|
import { PageHeader } from "@/components/shared/PageHeader";
|
|
@@ -44,6 +43,16 @@ type StatusFilter = "all" | TeamStatus;
|
|
|
type RunStatusFilter = "all" | TeamRunStatus;
|
|
type RunStatusFilter = "all" | TeamRunStatus;
|
|
|
type SortMode = "recent" | "name" | "status";
|
|
type SortMode = "recent" | "name" | "status";
|
|
|
type ViewMode = "list" | "grid";
|
|
type ViewMode = "list" | "grid";
|
|
|
|
|
+type MemberDraft = {
|
|
|
|
|
+ role: string;
|
|
|
|
|
+ agent_id: string;
|
|
|
|
|
+ responsibility: string;
|
|
|
|
|
+};
|
|
|
|
|
+type PolicyDraft = {
|
|
|
|
|
+ max_rounds: string;
|
|
|
|
|
+ handoff: string;
|
|
|
|
|
+ failure_mode: string;
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
const TEAM_TYPE_OPTIONS = [
|
|
const TEAM_TYPE_OPTIONS = [
|
|
|
{ value: "collaborative", label: "Collaborative" },
|
|
{ value: "collaborative", label: "Collaborative" },
|
|
@@ -65,6 +74,25 @@ const RUN_TEMPLATE_OPTIONS = [
|
|
|
"Compare two implementation options and return a decision with tradeoffs.",
|
|
"Compare two implementation options and return a decision with tradeoffs.",
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
|
|
+const DEFAULT_MEMBER_DRAFTS: MemberDraft[] = [
|
|
|
|
|
+ {
|
|
|
|
|
+ role: "supervisor",
|
|
|
|
|
+ agent_id: "agent_supervisor",
|
|
|
|
|
+ responsibility: "Plan work, route tasks, and merge final output.",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ role: "worker",
|
|
|
|
|
+ agent_id: "agent_worker",
|
|
|
|
|
+ responsibility: "Execute assigned specialist tasks.",
|
|
|
|
|
+ },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_POLICY_DRAFT: PolicyDraft = {
|
|
|
|
|
+ max_rounds: "3",
|
|
|
|
|
+ handoff: "supervisor",
|
|
|
|
|
+ failure_mode: "stop_on_critical",
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
export function TeamsPage() {
|
|
export function TeamsPage() {
|
|
|
const [teams, setTeams] = React.useState<TeamDefinition[]>([]);
|
|
const [teams, setTeams] = React.useState<TeamDefinition[]>([]);
|
|
|
const [versions, setVersions] = React.useState<TeamVersion[]>([]);
|
|
const [versions, setVersions] = React.useState<TeamVersion[]>([]);
|
|
@@ -80,9 +108,11 @@ export function TeamsPage() {
|
|
|
const [loading, setLoading] = React.useState(true);
|
|
const [loading, setLoading] = React.useState(true);
|
|
|
const [relatedLoading, setRelatedLoading] = React.useState(false);
|
|
const [relatedLoading, setRelatedLoading] = React.useState(false);
|
|
|
const [error, setError] = React.useState<string>();
|
|
const [error, setError] = React.useState<string>();
|
|
|
|
|
+ const [relatedError, setRelatedError] = React.useState<string>();
|
|
|
const [createOpen, setCreateOpen] = React.useState(false);
|
|
const [createOpen, setCreateOpen] = React.useState(false);
|
|
|
const [versionOpen, setVersionOpen] = React.useState(false);
|
|
const [versionOpen, setVersionOpen] = React.useState(false);
|
|
|
const [runOpen, setRunOpen] = React.useState(false);
|
|
const [runOpen, setRunOpen] = React.useState(false);
|
|
|
|
|
+ const detailsRequestRef = React.useRef(0);
|
|
|
|
|
|
|
|
const selectedTeam = teams.find((team) => team.id === selectedTeamId);
|
|
const selectedTeam = teams.find((team) => team.id === selectedTeamId);
|
|
|
const teamTypes = React.useMemo(() => Array.from(new Set(teams.map((team) => team.team_type))).sort(), [teams]);
|
|
const teamTypes = React.useMemo(() => Array.from(new Set(teams.map((team) => team.team_type))).sort(), [teams]);
|
|
@@ -132,13 +162,28 @@ export function TeamsPage() {
|
|
|
|
|
|
|
|
const reloadTeamDetails = React.useCallback(async () => {
|
|
const reloadTeamDetails = React.useCallback(async () => {
|
|
|
if (!selectedTeamId) return;
|
|
if (!selectedTeamId) return;
|
|
|
|
|
+ const requestId = detailsRequestRef.current + 1;
|
|
|
|
|
+ detailsRequestRef.current = requestId;
|
|
|
setRelatedLoading(true);
|
|
setRelatedLoading(true);
|
|
|
|
|
+ setRelatedError(undefined);
|
|
|
|
|
+ setVersions([]);
|
|
|
|
|
+ setRuns([]);
|
|
|
try {
|
|
try {
|
|
|
const [versionData, runData] = await Promise.all([listTeamVersions(selectedTeamId), listTeamRuns(selectedTeamId)]);
|
|
const [versionData, runData] = await Promise.all([listTeamVersions(selectedTeamId), listTeamRuns(selectedTeamId)]);
|
|
|
- setVersions(versionData);
|
|
|
|
|
- setRuns(runData);
|
|
|
|
|
|
|
+ if (detailsRequestRef.current === requestId) {
|
|
|
|
|
+ setVersions(versionData);
|
|
|
|
|
+ setRuns(runData);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (detailsRequestRef.current === requestId) {
|
|
|
|
|
+ const message = err instanceof Error ? err.message : "Failed to load team details";
|
|
|
|
|
+ setRelatedError(message);
|
|
|
|
|
+ toast.error(message);
|
|
|
|
|
+ }
|
|
|
} finally {
|
|
} finally {
|
|
|
- setRelatedLoading(false);
|
|
|
|
|
|
|
+ if (detailsRequestRef.current === requestId) {
|
|
|
|
|
+ setRelatedLoading(false);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}, [selectedTeamId]);
|
|
}, [selectedTeamId]);
|
|
|
|
|
|
|
@@ -148,10 +193,13 @@ export function TeamsPage() {
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
React.useEffect(() => {
|
|
|
if (!selectedTeamId) {
|
|
if (!selectedTeamId) {
|
|
|
|
|
+ detailsRequestRef.current += 1;
|
|
|
setVersions([]);
|
|
setVersions([]);
|
|
|
setRuns([]);
|
|
setRuns([]);
|
|
|
|
|
+ setRelatedError(undefined);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ setRunStatusFilter("all");
|
|
|
void reloadTeamDetails();
|
|
void reloadTeamDetails();
|
|
|
}, [reloadTeamDetails, selectedTeamId]);
|
|
}, [reloadTeamDetails, selectedTeamId]);
|
|
|
|
|
|
|
@@ -174,6 +222,22 @@ export function TeamsPage() {
|
|
|
setSortMode("recent");
|
|
setSortMode("recent");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function openVersionCreator() {
|
|
|
|
|
+ if (!selectedTeam) return;
|
|
|
|
|
+ setVersionOpen(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function openRunStarter() {
|
|
|
|
|
+ if (!selectedTeam) return;
|
|
|
|
|
+ if (!latestVersion) {
|
|
|
|
|
+ setActiveTab("versions");
|
|
|
|
|
+ setVersionOpen(true);
|
|
|
|
|
+ toast.info("Create a team version before starting a run");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ setRunOpen(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (loading) return <LoadingSpinner label="Loading teams" />;
|
|
if (loading) return <LoadingSpinner label="Loading teams" />;
|
|
|
if (error) return <ApiErrorState message={error} onRetry={() => void load()} />;
|
|
if (error) return <ApiErrorState message={error} onRetry={() => void load()} />;
|
|
|
|
|
|
|
@@ -326,78 +390,89 @@ export function TeamsPage() {
|
|
|
<Button variant="outline" disabled={!selectedTeam} onClick={() => void copyTeamCode()}>
|
|
<Button variant="outline" disabled={!selectedTeam} onClick={() => void copyTeamCode()}>
|
|
|
<Copy className="h-4 w-4" /> Copy Code
|
|
<Copy className="h-4 w-4" /> Copy Code
|
|
|
</Button>
|
|
</Button>
|
|
|
- <Button variant="secondary" disabled={!selectedTeam} onClick={() => setVersionOpen(true)}>
|
|
|
|
|
|
|
+ <Button variant="secondary" disabled={!selectedTeam} onClick={openVersionCreator}>
|
|
|
<FileCode2 className="h-4 w-4" /> New Version
|
|
<FileCode2 className="h-4 w-4" /> New Version
|
|
|
</Button>
|
|
</Button>
|
|
|
- <Button disabled={!selectedTeam || !latestVersion} onClick={() => setRunOpen(true)}>
|
|
|
|
|
- <Play className="h-4 w-4" /> Run
|
|
|
|
|
|
|
+ <Button disabled={!selectedTeam} onClick={openRunStarter}>
|
|
|
|
|
+ {latestVersion ? <Play className="h-4 w-4" /> : <FileCode2 className="h-4 w-4" />}
|
|
|
|
|
+ {latestVersion ? "Run" : "Create Version"}
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
<CardContent>
|
|
<CardContent>
|
|
|
{selectedTeam ? (
|
|
{selectedTeam ? (
|
|
|
- <Tabs
|
|
|
|
|
- value={activeTab}
|
|
|
|
|
- onChange={setActiveTab}
|
|
|
|
|
- tabs={[
|
|
|
|
|
- {
|
|
|
|
|
- value: "overview",
|
|
|
|
|
- label: "Overview",
|
|
|
|
|
- content: (
|
|
|
|
|
- <TeamOverview
|
|
|
|
|
- team={selectedTeam}
|
|
|
|
|
- latestVersion={latestVersion}
|
|
|
|
|
- versionCount={versions.length}
|
|
|
|
|
- runCount={sortedRuns.length}
|
|
|
|
|
- failedRunCount={failedRuns}
|
|
|
|
|
- latestRun={sortedRuns[0]}
|
|
|
|
|
- />
|
|
|
|
|
- ),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- value: "members",
|
|
|
|
|
- label: "Members",
|
|
|
|
|
- content: <TeamMembers version={latestVersion} loading={relatedLoading} onCreateVersion={() => setVersionOpen(true)} />,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- value: "versions",
|
|
|
|
|
- label: "Versions",
|
|
|
|
|
- content: <TeamVersions versions={sortedVersions} loading={relatedLoading} onCreateVersion={() => setVersionOpen(true)} />,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- value: "runs",
|
|
|
|
|
- label: "Runs",
|
|
|
|
|
- content: (
|
|
|
|
|
- <TeamRuns
|
|
|
|
|
- runs={filteredRuns}
|
|
|
|
|
- loading={relatedLoading}
|
|
|
|
|
- statusFilter={runStatusFilter}
|
|
|
|
|
- onStatusFilterChange={setRunStatusFilter}
|
|
|
|
|
- onStartRun={() => setRunOpen(true)}
|
|
|
|
|
- />
|
|
|
|
|
- ),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- value: "console",
|
|
|
|
|
- label: "Run Console",
|
|
|
|
|
- content: (
|
|
|
|
|
- <TeamRunConsole
|
|
|
|
|
- team={selectedTeam}
|
|
|
|
|
- versions={sortedVersions}
|
|
|
|
|
- runs={sortedRuns}
|
|
|
|
|
- loading={relatedLoading}
|
|
|
|
|
- onCreated={() => void reloadTeamDetails()}
|
|
|
|
|
- />
|
|
|
|
|
- ),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- value: "policy",
|
|
|
|
|
- label: "Policy",
|
|
|
|
|
- content: <TeamPolicy version={latestVersion} loading={relatedLoading} onCreateVersion={() => setVersionOpen(true)} />,
|
|
|
|
|
- },
|
|
|
|
|
- ]}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ {relatedError ? <InlineError message={relatedError} onRetry={() => void reloadTeamDetails()} /> : null}
|
|
|
|
|
+ <Tabs
|
|
|
|
|
+ value={activeTab}
|
|
|
|
|
+ onChange={setActiveTab}
|
|
|
|
|
+ tabs={[
|
|
|
|
|
+ {
|
|
|
|
|
+ value: "overview",
|
|
|
|
|
+ label: "Overview",
|
|
|
|
|
+ content: (
|
|
|
|
|
+ <TeamOverview
|
|
|
|
|
+ team={selectedTeam}
|
|
|
|
|
+ latestVersion={latestVersion}
|
|
|
|
|
+ versionCount={versions.length}
|
|
|
|
|
+ runCount={sortedRuns.length}
|
|
|
|
|
+ failedRunCount={failedRuns}
|
|
|
|
|
+ latestRun={sortedRuns[0]}
|
|
|
|
|
+ loading={relatedLoading}
|
|
|
|
|
+ />
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: "members",
|
|
|
|
|
+ label: "Members",
|
|
|
|
|
+ content: <TeamMembers version={latestVersion} loading={relatedLoading} onCreateVersion={openVersionCreator} />,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: "versions",
|
|
|
|
|
+ label: "Versions",
|
|
|
|
|
+ content: <TeamVersions versions={sortedVersions} loading={relatedLoading} onCreateVersion={openVersionCreator} />,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: "runs",
|
|
|
|
|
+ label: "Runs",
|
|
|
|
|
+ content: (
|
|
|
|
|
+ <TeamRuns
|
|
|
|
|
+ runs={filteredRuns}
|
|
|
|
|
+ loading={relatedLoading}
|
|
|
|
|
+ statusFilter={runStatusFilter}
|
|
|
|
|
+ onStatusFilterChange={setRunStatusFilter}
|
|
|
|
|
+ onStartRun={openRunStarter}
|
|
|
|
|
+ canStartRun={Boolean(latestVersion)}
|
|
|
|
|
+ />
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: "console",
|
|
|
|
|
+ label: "Run Console",
|
|
|
|
|
+ content: (
|
|
|
|
|
+ <TeamRunConsole
|
|
|
|
|
+ team={selectedTeam}
|
|
|
|
|
+ versions={sortedVersions}
|
|
|
|
|
+ runs={sortedRuns}
|
|
|
|
|
+ loading={relatedLoading}
|
|
|
|
|
+ onCreated={(run) => {
|
|
|
|
|
+ setRuns((current) => [run, ...current]);
|
|
|
|
|
+ setActiveTab("runs");
|
|
|
|
|
+ void reloadTeamDetails();
|
|
|
|
|
+ }}
|
|
|
|
|
+ onCreateVersion={openVersionCreator}
|
|
|
|
|
+ />
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: "policy",
|
|
|
|
|
+ label: "Policy",
|
|
|
|
|
+ content: <TeamPolicy version={latestVersion} loading={relatedLoading} onCreateVersion={openVersionCreator} />,
|
|
|
|
|
+ },
|
|
|
|
|
+ ]}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
) : (
|
|
) : (
|
|
|
<EmptyState icon={Bot} title="No teams" description="Create a team to start coordinating multiple specialized agents." actionLabel="New Team" onAction={() => setCreateOpen(true)} />
|
|
<EmptyState icon={Bot} title="No teams" description="Create a team to start coordinating multiple specialized agents." actionLabel="New Team" onAction={() => setCreateOpen(true)} />
|
|
|
)}
|
|
)}
|
|
@@ -405,19 +480,37 @@ export function TeamsPage() {
|
|
|
</Card>
|
|
</Card>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <CreateTeamDialog open={createOpen} onOpenChange={setCreateOpen} onCreated={() => void load()} />
|
|
|
|
|
|
|
+ <CreateTeamDialog
|
|
|
|
|
+ open={createOpen}
|
|
|
|
|
+ onOpenChange={setCreateOpen}
|
|
|
|
|
+ onCreated={(team) => {
|
|
|
|
|
+ setSelectedTeamId(team.id);
|
|
|
|
|
+ setActiveTab("overview");
|
|
|
|
|
+ setSearch("");
|
|
|
|
|
+ void load();
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
<CreateTeamVersionDialog
|
|
<CreateTeamVersionDialog
|
|
|
open={versionOpen}
|
|
open={versionOpen}
|
|
|
onOpenChange={setVersionOpen}
|
|
onOpenChange={setVersionOpen}
|
|
|
teamId={selectedTeamId}
|
|
teamId={selectedTeamId}
|
|
|
- onCreated={() => void reloadTeamDetails()}
|
|
|
|
|
|
|
+ onCreated={(version) => {
|
|
|
|
|
+ setVersions((current) => [version, ...current]);
|
|
|
|
|
+ setActiveTab("console");
|
|
|
|
|
+ void reloadTeamDetails();
|
|
|
|
|
+ }}
|
|
|
/>
|
|
/>
|
|
|
<CreateTeamRunDialog
|
|
<CreateTeamRunDialog
|
|
|
open={runOpen}
|
|
open={runOpen}
|
|
|
onOpenChange={setRunOpen}
|
|
onOpenChange={setRunOpen}
|
|
|
teamId={selectedTeamId}
|
|
teamId={selectedTeamId}
|
|
|
versions={sortedVersions}
|
|
versions={sortedVersions}
|
|
|
- onCreated={() => void reloadTeamDetails()}
|
|
|
|
|
|
|
+ onCreateVersion={openVersionCreator}
|
|
|
|
|
+ onCreated={(run) => {
|
|
|
|
|
+ setRuns((current) => [run, ...current]);
|
|
|
|
|
+ setActiveTab("runs");
|
|
|
|
|
+ void reloadTeamDetails();
|
|
|
|
|
+ }}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
@@ -448,6 +541,17 @@ function TeamCard({ team, active, onOpen }: { team: TeamDefinition; active: bool
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function InlineError({ message, onRetry }: { message: string; onRetry: () => void }) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex flex-col gap-3 rounded-md border border-red-500/25 bg-red-500/5 p-3 text-sm sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
+ <p className="text-foreground">{message}</p>
|
|
|
|
|
+ <Button type="button" size="sm" variant="outline" onClick={onRetry}>
|
|
|
|
|
+ <RefreshCw className="h-4 w-4" /> Retry
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function TeamOverview({
|
|
function TeamOverview({
|
|
|
team,
|
|
team,
|
|
|
latestVersion,
|
|
latestVersion,
|
|
@@ -455,6 +559,7 @@ function TeamOverview({
|
|
|
runCount,
|
|
runCount,
|
|
|
failedRunCount,
|
|
failedRunCount,
|
|
|
latestRun,
|
|
latestRun,
|
|
|
|
|
+ loading,
|
|
|
}: {
|
|
}: {
|
|
|
team: TeamDefinition;
|
|
team: TeamDefinition;
|
|
|
latestVersion?: TeamVersion;
|
|
latestVersion?: TeamVersion;
|
|
@@ -462,14 +567,15 @@ function TeamOverview({
|
|
|
runCount: number;
|
|
runCount: number;
|
|
|
failedRunCount: number;
|
|
failedRunCount: number;
|
|
|
latestRun?: TeamRun;
|
|
latestRun?: TeamRun;
|
|
|
|
|
+ loading: boolean;
|
|
|
}) {
|
|
}) {
|
|
|
return (
|
|
return (
|
|
|
<div className="space-y-5">
|
|
<div className="space-y-5">
|
|
|
<div className="grid gap-4 md:grid-cols-4">
|
|
<div className="grid gap-4 md:grid-cols-4">
|
|
|
- <SummaryTile label="Versions" value={versionCount} />
|
|
|
|
|
- <SummaryTile label="Runs" value={runCount} />
|
|
|
|
|
- <SummaryTile label="Failures" value={failedRunCount} />
|
|
|
|
|
- <SummaryTile label="Latest" value={latestVersion ? `v${latestVersion.version_no}` : "None"} />
|
|
|
|
|
|
|
+ <SummaryTile label="Versions" value={loading ? "..." : versionCount} />
|
|
|
|
|
+ <SummaryTile label="Runs" value={loading ? "..." : runCount} />
|
|
|
|
|
+ <SummaryTile label="Failures" value={loading ? "..." : failedRunCount} />
|
|
|
|
|
+ <SummaryTile label="Latest" value={loading ? "..." : latestVersion ? `v${latestVersion.version_no}` : "None"} />
|
|
|
</div>
|
|
</div>
|
|
|
<div className="grid gap-4 text-sm md:grid-cols-2">
|
|
<div className="grid gap-4 text-sm md:grid-cols-2">
|
|
|
<Detail label="Code" value={team.code} mono />
|
|
<Detail label="Code" value={team.code} mono />
|
|
@@ -506,29 +612,29 @@ function TeamMembers({
|
|
|
}
|
|
}
|
|
|
if (!version.member_refs_json.length) {
|
|
if (!version.member_refs_json.length) {
|
|
|
return (
|
|
return (
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <EmptyState icon={Users} title="No members configured" description="This version has no member references yet. The policy and members can still be inspected from the raw version payload." />
|
|
|
|
|
- <JsonViewer value={version.member_refs_json} collapsed={false} />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <EmptyState
|
|
|
|
|
+ icon={Users}
|
|
|
|
|
+ title="No members configured"
|
|
|
|
|
+ description="Create a new version with member rows to define who participates and what each role is responsible for."
|
|
|
|
|
+ actionLabel="New Version"
|
|
|
|
|
+ onAction={onCreateVersion}
|
|
|
|
|
+ />
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
return (
|
|
return (
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <div className="grid gap-3 md:grid-cols-2">
|
|
|
|
|
- {version.member_refs_json.map((member, index) => (
|
|
|
|
|
- <div key={index} className="rounded-md border border-border bg-muted/30 p-4">
|
|
|
|
|
- <div className="flex items-start justify-between gap-3">
|
|
|
|
|
- <div className="min-w-0">
|
|
|
|
|
- <p className="truncate text-sm font-semibold">{getJsonString(member, "name") ?? getJsonString(member, "agent_name") ?? `Member ${index + 1}`}</p>
|
|
|
|
|
- <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{getJsonString(member, "agent_id") ?? getJsonString(member, "id") ?? "unbound-agent"}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Badge className="border-border bg-muted/60 text-muted-foreground">{getJsonString(member, "role") ?? "member"}</Badge>
|
|
|
|
|
|
|
+ <div className="grid gap-3 md:grid-cols-2">
|
|
|
|
|
+ {version.member_refs_json.map((member, index) => (
|
|
|
|
|
+ <div key={index} className="rounded-md border border-border bg-muted/30 p-4">
|
|
|
|
|
+ <div className="flex items-start justify-between gap-3">
|
|
|
|
|
+ <div className="min-w-0">
|
|
|
|
|
+ <p className="truncate text-sm font-semibold">{getJsonString(member, "name") ?? getJsonString(member, "agent_name") ?? `Member ${index + 1}`}</p>
|
|
|
|
|
+ <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{getJsonString(member, "agent_id") ?? getJsonString(member, "id") ?? "unbound-agent"}</p>
|
|
|
</div>
|
|
</div>
|
|
|
- <p className="mt-3 text-sm leading-6 text-muted-foreground">{getJsonString(member, "description") ?? getJsonString(member, "responsibility") ?? "No responsibility summary provided."}</p>
|
|
|
|
|
|
|
+ <Badge className="border-border bg-muted/60 text-muted-foreground">{getJsonString(member, "role") ?? "member"}</Badge>
|
|
|
</div>
|
|
</div>
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- <JsonViewer value={version.member_refs_json} />
|
|
|
|
|
|
|
+ <p className="mt-3 text-sm leading-6 text-muted-foreground">{getJsonString(member, "description") ?? getJsonString(member, "responsibility") ?? "No responsibility summary provided."}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -578,12 +684,14 @@ function TeamRuns({
|
|
|
statusFilter,
|
|
statusFilter,
|
|
|
onStatusFilterChange,
|
|
onStatusFilterChange,
|
|
|
onStartRun,
|
|
onStartRun,
|
|
|
|
|
+ canStartRun,
|
|
|
}: {
|
|
}: {
|
|
|
runs: TeamRun[];
|
|
runs: TeamRun[];
|
|
|
loading: boolean;
|
|
loading: boolean;
|
|
|
statusFilter: RunStatusFilter;
|
|
statusFilter: RunStatusFilter;
|
|
|
onStatusFilterChange: (status: RunStatusFilter) => void;
|
|
onStatusFilterChange: (status: RunStatusFilter) => void;
|
|
|
onStartRun: () => void;
|
|
onStartRun: () => void;
|
|
|
|
|
+ canStartRun: boolean;
|
|
|
}) {
|
|
}) {
|
|
|
if (loading) return <LoadingSpinner label="Loading runs" />;
|
|
if (loading) return <LoadingSpinner label="Loading runs" />;
|
|
|
return (
|
|
return (
|
|
@@ -604,7 +712,8 @@ function TeamRuns({
|
|
|
]}
|
|
]}
|
|
|
/>
|
|
/>
|
|
|
<Button type="button" variant="secondary" onClick={onStartRun}>
|
|
<Button type="button" variant="secondary" onClick={onStartRun}>
|
|
|
- <Play className="h-4 w-4" /> Start Run
|
|
|
|
|
|
|
+ {canStartRun ? <Play className="h-4 w-4" /> : <FileCode2 className="h-4 w-4" />}
|
|
|
|
|
+ {canStartRun ? "Start Run" : "Create Version"}
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
|
{runs.length ? (
|
|
{runs.length ? (
|
|
@@ -627,7 +736,13 @@ function TeamRuns({
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
|
) : (
|
|
) : (
|
|
|
- <EmptyState icon={Activity} title="No runs" description="No run records match this team and status filter." actionLabel="Start Run" onAction={onStartRun} />
|
|
|
|
|
|
|
+ <EmptyState
|
|
|
|
|
+ icon={Activity}
|
|
|
|
|
+ title="No runs"
|
|
|
|
|
+ description={canStartRun ? "No run records match this team and status filter." : "Create a version before this team can run work."}
|
|
|
|
|
+ actionLabel={canStartRun ? "Start Run" : "Create Version"}
|
|
|
|
|
+ onAction={onStartRun}
|
|
|
|
|
+ />
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
@@ -639,12 +754,14 @@ function TeamRunConsole({
|
|
|
runs,
|
|
runs,
|
|
|
loading,
|
|
loading,
|
|
|
onCreated,
|
|
onCreated,
|
|
|
|
|
+ onCreateVersion,
|
|
|
}: {
|
|
}: {
|
|
|
team: TeamDefinition;
|
|
team: TeamDefinition;
|
|
|
versions: TeamVersion[];
|
|
versions: TeamVersion[];
|
|
|
runs: TeamRun[];
|
|
runs: TeamRun[];
|
|
|
loading: boolean;
|
|
loading: boolean;
|
|
|
- onCreated: () => void;
|
|
|
|
|
|
|
+ onCreated: (run: TeamRun) => void;
|
|
|
|
|
+ onCreateVersion: () => void;
|
|
|
}) {
|
|
}) {
|
|
|
const tenantId = useAuthStore((state) => state.tenantId);
|
|
const tenantId = useAuthStore((state) => state.tenantId);
|
|
|
const [versionId, setVersionId] = React.useState(versions[0]?.id ?? "");
|
|
const [versionId, setVersionId] = React.useState(versions[0]?.id ?? "");
|
|
@@ -660,10 +777,12 @@ function TeamRunConsole({
|
|
|
if (!versionId || !inputText.trim()) return;
|
|
if (!versionId || !inputText.trim()) return;
|
|
|
setSubmitting(true);
|
|
setSubmitting(true);
|
|
|
try {
|
|
try {
|
|
|
- await createTeamRun({ tenant_id: tenantId, team_id: team.id, team_version_id: versionId, input_text: inputText });
|
|
|
|
|
|
|
+ const run = await createTeamRun({ tenant_id: tenantId, team_id: team.id, team_version_id: versionId, input_text: inputText });
|
|
|
toast.success("Team run started");
|
|
toast.success("Team run started");
|
|
|
setInputText("");
|
|
setInputText("");
|
|
|
- onCreated();
|
|
|
|
|
|
|
+ onCreated(run);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ toast.error(err instanceof Error ? err.message : "Failed to start team run");
|
|
|
} finally {
|
|
} finally {
|
|
|
setSubmitting(false);
|
|
setSubmitting(false);
|
|
|
}
|
|
}
|
|
@@ -671,7 +790,15 @@ function TeamRunConsole({
|
|
|
|
|
|
|
|
if (loading) return <LoadingSpinner label="Preparing console" />;
|
|
if (loading) return <LoadingSpinner label="Preparing console" />;
|
|
|
if (!versions.length) {
|
|
if (!versions.length) {
|
|
|
- return <EmptyState icon={Play} title="No runnable version" description="Create a team version before starting a collaborative run." />;
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <EmptyState
|
|
|
|
|
+ icon={Play}
|
|
|
|
|
+ title="No runnable version"
|
|
|
|
|
+ description="Create a team version before starting a collaborative run."
|
|
|
|
|
+ actionLabel="Create Version"
|
|
|
|
|
+ onAction={onCreateVersion}
|
|
|
|
|
+ />
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
@@ -739,7 +866,7 @@ function TeamPolicy({
|
|
|
return <EmptyState icon={Settings2} title="No policy" description="Create a team version to inspect member policy and coordination settings." actionLabel="New Version" onAction={onCreateVersion} />;
|
|
return <EmptyState icon={Settings2} title="No policy" description="Create a team version to inspect member policy and coordination settings." actionLabel="New Version" onAction={onCreateVersion} />;
|
|
|
}
|
|
}
|
|
|
return (
|
|
return (
|
|
|
- <div className="grid gap-4 lg:grid-cols-2">
|
|
|
|
|
|
|
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
|
<section className="rounded-md border border-border bg-muted/30 p-4">
|
|
<section className="rounded-md border border-border bg-muted/30 p-4">
|
|
|
<div className="mb-3 flex items-center gap-2">
|
|
<div className="mb-3 flex items-center gap-2">
|
|
|
<Layers3 className="h-4 w-4 text-muted-foreground" />
|
|
<Layers3 className="h-4 w-4 text-muted-foreground" />
|
|
@@ -751,11 +878,22 @@ function TeamPolicy({
|
|
|
<Detail label="Status" value={version.status} />
|
|
<Detail label="Status" value={version.status} />
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
- <JsonViewer value={version.policy_json} collapsed={false} />
|
|
|
|
|
|
|
+ <PolicyTile label="Max Rounds" value={getJsonString(version.policy_json, "max_rounds") ?? "Not set"} />
|
|
|
|
|
+ <PolicyTile label="Handoff" value={readableLabel(getJsonString(version.policy_json, "handoff") ?? "Not set")} />
|
|
|
|
|
+ <PolicyTile label="Failure Mode" value={readableLabel(getJsonString(version.policy_json, "failure_mode") ?? "Not set")} />
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function PolicyTile({ label, value }: { label: string; value: string }) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <section className="rounded-md border border-border bg-muted/30 p-4">
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">{label}</p>
|
|
|
|
|
+ <p className="mt-2 text-lg font-semibold">{value}</p>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
|
function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
|
|
return (
|
|
return (
|
|
|
<div className="min-w-0">
|
|
<div className="min-w-0">
|
|
@@ -783,22 +921,43 @@ function CreateTeamVersionDialog({
|
|
|
open: boolean;
|
|
open: boolean;
|
|
|
onOpenChange: (open: boolean) => void;
|
|
onOpenChange: (open: boolean) => void;
|
|
|
teamId?: string;
|
|
teamId?: string;
|
|
|
- onCreated: () => void;
|
|
|
|
|
|
|
+ onCreated: (version: TeamVersion) => void;
|
|
|
}) {
|
|
}) {
|
|
|
const tenantId = useAuthStore((state) => state.tenantId);
|
|
const tenantId = useAuthStore((state) => state.tenantId);
|
|
|
const [form, setForm] = React.useState({ coordination_mode: "supervisor", objective: "" });
|
|
const [form, setForm] = React.useState({ coordination_mode: "supervisor", objective: "" });
|
|
|
|
|
+ const [members, setMembers] = React.useState<MemberDraft[]>(DEFAULT_MEMBER_DRAFTS);
|
|
|
|
|
+ const [policy, setPolicy] = React.useState<PolicyDraft>(DEFAULT_POLICY_DRAFT);
|
|
|
|
|
+ const [formError, setFormError] = React.useState<string>();
|
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
|
|
|
|
|
|
async function submit(event: React.FormEvent) {
|
|
async function submit(event: React.FormEvent) {
|
|
|
event.preventDefault();
|
|
event.preventDefault();
|
|
|
if (!teamId) return;
|
|
if (!teamId) return;
|
|
|
|
|
+ const versionPayload = buildVersionConfig(members, policy);
|
|
|
|
|
+ if (!versionPayload.ok) {
|
|
|
|
|
+ setFormError(versionPayload.message);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
setSubmitting(true);
|
|
setSubmitting(true);
|
|
|
|
|
+ setFormError(undefined);
|
|
|
try {
|
|
try {
|
|
|
- await createTeamVersion({ tenant_id: tenantId, team_id: teamId, ...form, status: "draft" });
|
|
|
|
|
|
|
+ const version = await createTeamVersion({
|
|
|
|
|
+ tenant_id: tenantId,
|
|
|
|
|
+ team_id: teamId,
|
|
|
|
|
+ coordination_mode: form.coordination_mode,
|
|
|
|
|
+ objective: form.objective,
|
|
|
|
|
+ member_refs: versionPayload.memberRefs,
|
|
|
|
|
+ policy_json: versionPayload.policyJson,
|
|
|
|
|
+ status: "draft",
|
|
|
|
|
+ });
|
|
|
toast.success("Team version created");
|
|
toast.success("Team version created");
|
|
|
onOpenChange(false);
|
|
onOpenChange(false);
|
|
|
setForm({ coordination_mode: "supervisor", objective: "" });
|
|
setForm({ coordination_mode: "supervisor", objective: "" });
|
|
|
- onCreated();
|
|
|
|
|
|
|
+ setMembers(DEFAULT_MEMBER_DRAFTS);
|
|
|
|
|
+ setPolicy(DEFAULT_POLICY_DRAFT);
|
|
|
|
|
+ onCreated(version);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ toast.error(err instanceof Error ? err.message : "Failed to create team version");
|
|
|
} finally {
|
|
} finally {
|
|
|
setSubmitting(false);
|
|
setSubmitting(false);
|
|
|
}
|
|
}
|
|
@@ -821,9 +980,9 @@ function CreateTeamVersionDialog({
|
|
|
onChange={(event) => setForm({ ...form, objective: event.target.value })}
|
|
onChange={(event) => setForm({ ...form, objective: event.target.value })}
|
|
|
/>
|
|
/>
|
|
|
</Field>
|
|
</Field>
|
|
|
- <div className="rounded-md border border-border bg-muted/40 p-3 text-sm text-muted-foreground">
|
|
|
|
|
- Members and policy start empty so the version can be refined without blocking creation.
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <MemberDraftEditor members={members} onChange={setMembers} />
|
|
|
|
|
+ <PolicyDraftEditor policy={policy} onChange={setPolicy} />
|
|
|
|
|
+ {formError ? <p className="rounded-md border border-red-500/25 bg-red-500/5 p-3 text-sm text-foreground">{formError}</p> : null}
|
|
|
<div className="flex justify-end gap-2">
|
|
<div className="flex justify-end gap-2">
|
|
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
|
|
Cancel
|
|
Cancel
|
|
@@ -835,18 +994,122 @@ function CreateTeamVersionDialog({
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function MemberDraftEditor({ members, onChange }: { members: MemberDraft[]; onChange: (members: MemberDraft[]) => void }) {
|
|
|
|
|
+ function updateMember(index: number, patch: Partial<MemberDraft>) {
|
|
|
|
|
+ onChange(members.map((member, memberIndex) => (memberIndex === index ? { ...member, ...patch } : member)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function removeMember(index: number) {
|
|
|
|
|
+ onChange(members.filter((_, memberIndex) => memberIndex !== index));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <section className="space-y-3">
|
|
|
|
|
+ <div className="flex items-center justify-between gap-3">
|
|
|
|
|
+ <h3 className="text-sm font-medium">Members</h3>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ onClick={() => onChange([...members, { role: "worker", agent_id: "", responsibility: "" }])}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Users className="h-4 w-4" /> Add Member
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="space-y-3">
|
|
|
|
|
+ {members.map((member, index) => (
|
|
|
|
|
+ <div key={index} className="rounded-md border border-border bg-muted/30 p-3">
|
|
|
|
|
+ <div className="grid gap-3 md:grid-cols-[150px_1fr_auto]">
|
|
|
|
|
+ <Select
|
|
|
|
|
+ aria-label={`Role for member ${index + 1}`}
|
|
|
|
|
+ value={member.role}
|
|
|
|
|
+ onChange={(event) => updateMember(index, { role: event.target.value })}
|
|
|
|
|
+ options={[
|
|
|
|
|
+ { value: "supervisor", label: "Supervisor" },
|
|
|
|
|
+ { value: "worker", label: "Worker" },
|
|
|
|
|
+ { value: "reviewer", label: "Reviewer" },
|
|
|
|
|
+ { value: "planner", label: "Planner" },
|
|
|
|
|
+ ]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ aria-label={`Agent id for member ${index + 1}`}
|
|
|
|
|
+ value={member.agent_id}
|
|
|
|
|
+ placeholder="Agent ID"
|
|
|
|
|
+ onChange={(event) => updateMember(index, { agent_id: event.target.value })}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button type="button" size="sm" variant="ghost" disabled={members.length <= 1} onClick={() => removeMember(index)}>
|
|
|
|
|
+ Remove
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Textarea
|
|
|
|
|
+ className="mt-3 min-h-20"
|
|
|
|
|
+ aria-label={`Responsibility for member ${index + 1}`}
|
|
|
|
|
+ value={member.responsibility}
|
|
|
|
|
+ placeholder="Responsibility"
|
|
|
|
|
+ onChange={(event) => updateMember(index, { responsibility: event.target.value })}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function PolicyDraftEditor({ policy, onChange }: { policy: PolicyDraft; onChange: (policy: PolicyDraft) => void }) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <section className="space-y-3">
|
|
|
|
|
+ <h3 className="text-sm font-medium">Policy</h3>
|
|
|
|
|
+ <div className="grid gap-3 md:grid-cols-3">
|
|
|
|
|
+ <Field label="Max rounds">
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ min={1}
|
|
|
|
|
+ max={20}
|
|
|
|
|
+ value={policy.max_rounds}
|
|
|
|
|
+ onChange={(event) => onChange({ ...policy, max_rounds: event.target.value })}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Field>
|
|
|
|
|
+ <Field label="Handoff">
|
|
|
|
|
+ <Select
|
|
|
|
|
+ value={policy.handoff}
|
|
|
|
|
+ onChange={(event) => onChange({ ...policy, handoff: event.target.value })}
|
|
|
|
|
+ options={[
|
|
|
|
|
+ { value: "supervisor", label: "Supervisor" },
|
|
|
|
|
+ { value: "round_robin", label: "Round Robin" },
|
|
|
|
|
+ { value: "parallel_merge", label: "Parallel Merge" },
|
|
|
|
|
+ ]}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Field>
|
|
|
|
|
+ <Field label="Failure mode">
|
|
|
|
|
+ <Select
|
|
|
|
|
+ value={policy.failure_mode}
|
|
|
|
|
+ onChange={(event) => onChange({ ...policy, failure_mode: event.target.value })}
|
|
|
|
|
+ options={[
|
|
|
|
|
+ { value: "stop_on_critical", label: "Stop on Critical" },
|
|
|
|
|
+ { value: "continue_with_warning", label: "Continue with Warning" },
|
|
|
|
|
+ { value: "retry_once", label: "Retry Once" },
|
|
|
|
|
+ ]}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Field>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function CreateTeamRunDialog({
|
|
function CreateTeamRunDialog({
|
|
|
open,
|
|
open,
|
|
|
onOpenChange,
|
|
onOpenChange,
|
|
|
teamId,
|
|
teamId,
|
|
|
versions,
|
|
versions,
|
|
|
|
|
+ onCreateVersion,
|
|
|
onCreated,
|
|
onCreated,
|
|
|
}: {
|
|
}: {
|
|
|
open: boolean;
|
|
open: boolean;
|
|
|
onOpenChange: (open: boolean) => void;
|
|
onOpenChange: (open: boolean) => void;
|
|
|
teamId?: string;
|
|
teamId?: string;
|
|
|
versions: TeamVersion[];
|
|
versions: TeamVersion[];
|
|
|
- onCreated: () => void;
|
|
|
|
|
|
|
+ onCreateVersion: () => void;
|
|
|
|
|
+ onCreated: (run: TeamRun) => void;
|
|
|
}) {
|
|
}) {
|
|
|
const tenantId = useAuthStore((state) => state.tenantId);
|
|
const tenantId = useAuthStore((state) => state.tenantId);
|
|
|
const [versionId, setVersionId] = React.useState(versions[0]?.id ?? "");
|
|
const [versionId, setVersionId] = React.useState(versions[0]?.id ?? "");
|
|
@@ -859,19 +1122,38 @@ function CreateTeamRunDialog({
|
|
|
|
|
|
|
|
async function submit(event: React.FormEvent) {
|
|
async function submit(event: React.FormEvent) {
|
|
|
event.preventDefault();
|
|
event.preventDefault();
|
|
|
- if (!teamId || !versionId) return;
|
|
|
|
|
|
|
+ if (!teamId || !versionId || !inputText.trim()) return;
|
|
|
setSubmitting(true);
|
|
setSubmitting(true);
|
|
|
try {
|
|
try {
|
|
|
- await createTeamRun({ tenant_id: tenantId, team_id: teamId, team_version_id: versionId, input_text: inputText });
|
|
|
|
|
|
|
+ const run = await createTeamRun({ tenant_id: tenantId, team_id: teamId, team_version_id: versionId, input_text: inputText });
|
|
|
toast.success("Team run started");
|
|
toast.success("Team run started");
|
|
|
onOpenChange(false);
|
|
onOpenChange(false);
|
|
|
setInputText("");
|
|
setInputText("");
|
|
|
- onCreated();
|
|
|
|
|
|
|
+ onCreated(run);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ toast.error(err instanceof Error ? err.message : "Failed to start team run");
|
|
|
} finally {
|
|
} finally {
|
|
|
setSubmitting(false);
|
|
setSubmitting(false);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (!versions.length) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Dialog open={open} onOpenChange={onOpenChange} title="Start Team Run">
|
|
|
|
|
+ <EmptyState
|
|
|
|
|
+ icon={Play}
|
|
|
|
|
+ title="No runnable version"
|
|
|
|
|
+ description="Create a team version before starting a collaborative run."
|
|
|
|
|
+ actionLabel="Create Version"
|
|
|
|
|
+ onAction={() => {
|
|
|
|
|
+ onOpenChange(false);
|
|
|
|
|
+ onCreateVersion();
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<Dialog open={open} onOpenChange={onOpenChange} title="Start Team Run">
|
|
<Dialog open={open} onOpenChange={onOpenChange} title="Start Team Run">
|
|
|
<form className="space-y-4" onSubmit={submit}>
|
|
<form className="space-y-4" onSubmit={submit}>
|
|
@@ -892,14 +1174,14 @@ function CreateTeamRunDialog({
|
|
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
|
|
Cancel
|
|
Cancel
|
|
|
</Button>
|
|
</Button>
|
|
|
- <Button disabled={submitting || !teamId || !versionId}>{submitting ? "Starting..." : "Start Run"}</Button>
|
|
|
|
|
|
|
+ <Button disabled={submitting || !teamId || !versionId || !inputText.trim()}>{submitting ? "Starting..." : "Start Run"}</Button>
|
|
|
</div>
|
|
</div>
|
|
|
</form>
|
|
</form>
|
|
|
</Dialog>
|
|
</Dialog>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function CreateTeamDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (open: boolean) => void; onCreated: () => void }) {
|
|
|
|
|
|
|
+function CreateTeamDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (open: boolean) => void; onCreated: (team: TeamDefinition) => void }) {
|
|
|
const { tenantId, userId } = useAuthStore();
|
|
const { tenantId, userId } = useAuthStore();
|
|
|
const [form, setForm] = React.useState({ name: "", description: "", team_type: "collaborative" });
|
|
const [form, setForm] = React.useState({ name: "", description: "", team_type: "collaborative" });
|
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
@@ -908,11 +1190,13 @@ function CreateTeamDialog({ open, onOpenChange, onCreated }: { open: boolean; on
|
|
|
event.preventDefault();
|
|
event.preventDefault();
|
|
|
setSubmitting(true);
|
|
setSubmitting(true);
|
|
|
try {
|
|
try {
|
|
|
- await createTeam({ tenant_id: tenantId, owner_user_id: userId, ...form });
|
|
|
|
|
|
|
+ const team = await createTeam({ tenant_id: tenantId, owner_user_id: userId, ...form });
|
|
|
toast.success("Team created");
|
|
toast.success("Team created");
|
|
|
onOpenChange(false);
|
|
onOpenChange(false);
|
|
|
setForm({ name: "", description: "", team_type: "collaborative" });
|
|
setForm({ name: "", description: "", team_type: "collaborative" });
|
|
|
- onCreated();
|
|
|
|
|
|
|
+ onCreated(team);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ toast.error(err instanceof Error ? err.message : "Failed to create team");
|
|
|
} finally {
|
|
} finally {
|
|
|
setSubmitting(false);
|
|
setSubmitting(false);
|
|
|
}
|
|
}
|
|
@@ -963,3 +1247,34 @@ function getJsonString(value: JSONObject, key: string) {
|
|
|
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") return String(item);
|
|
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") return String(item);
|
|
|
return undefined;
|
|
return undefined;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+function buildVersionConfig(members: MemberDraft[], policy: PolicyDraft):
|
|
|
|
|
+ | { ok: true; memberRefs: JSONObject[]; policyJson: JSONObject }
|
|
|
|
|
+ | { ok: false; message: string } {
|
|
|
|
|
+ const normalizedMembers = members
|
|
|
|
|
+ .map((member) => ({
|
|
|
|
|
+ role: member.role.trim(),
|
|
|
|
|
+ agent_id: member.agent_id.trim(),
|
|
|
|
|
+ responsibility: member.responsibility.trim(),
|
|
|
|
|
+ }))
|
|
|
|
|
+ .filter((member) => member.role || member.agent_id || member.responsibility);
|
|
|
|
|
+ if (!normalizedMembers.length) {
|
|
|
|
|
+ return { ok: false, message: "Add at least one team member." };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (normalizedMembers.some((member) => !member.agent_id)) {
|
|
|
|
|
+ return { ok: false, message: "Each member needs an Agent ID." };
|
|
|
|
|
+ }
|
|
|
|
|
+ const maxRounds = Number(policy.max_rounds);
|
|
|
|
|
+ if (!Number.isInteger(maxRounds) || maxRounds < 1 || maxRounds > 20) {
|
|
|
|
|
+ return { ok: false, message: "Max rounds must be a whole number from 1 to 20." };
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ ok: true,
|
|
|
|
|
+ memberRefs: normalizedMembers,
|
|
|
|
|
+ policyJson: {
|
|
|
|
|
+ max_rounds: maxRounds,
|
|
|
|
|
+ handoff: policy.handoff,
|
|
|
|
|
+ failure_mode: policy.failure_mode,
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+}
|