- {!agent.locked && (
-
-
- Name
-
-
-
- {nameError && (
-
{nameError}
- )}
-
- )}
-
-
-
- Description
-
-
-
-
-
-
- Agent Type
-
-
-
-
-
Agent Types
- Conversation agents' responses are user-facing. You can use conversation agents for multi-turn conversations with users.
-
-
- Task agents' responses are internal and available to other agents. You can use them to build pipelines and DAGs within workflows. E.g. Conversation Agent {'->'} Task Agent {'->'} Task Agent.
-
-
-
-
-
handleUpdate({
- ...agent,
- outputVisibility: value as z.infer['outputVisibility']
- })}
- />
-
-
-
-
-
- Model
-
- {eligibleModels === "*" &&
-
-
-
Model Configuration
- Set this according to the PROVIDER_BASE_URL you have set in your .env file (such as your LiteLLM, gateway).
-
-
- E.g. LiteLLM's naming convention is like: 'claude-3-7-sonnet-latest', but you may have set alias model names or might be using a different provider like openrouter, openai etc.
-
-
- By default, the model is set to gpt-4.1, assuming your OpenAI API key is set in PROVIDER_API_KEY and PROVIDER_BASE_URL is not set.
-
-
-
}
-
-
- {eligibleModels === "*" && handleUpdate({
- ...agent,
- model: e.target.value as z.infer['model']
- })}
- className="w-full max-w-64"
- />}
- {eligibleModels !== "*" && {
- const key = keys.currentKey as string;
- const model = eligibleModels.find((m) => m.name === key);
- if (!model) {
- return;
- }
- if (!model.eligible) {
- setBillingError(`Please upgrade to the ${model.plan.toUpperCase()} plan to use this model.`);
- return;
- }
- handleUpdate({
- ...agent,
- model: key as z.infer['model']
- });
- }}
- >
-
- {eligibleModels.filter((model) => model.eligible).map((model) => (
-
- {model.name}
-
- ))}
-
-
- {eligibleModels.filter((model) => !model.eligible).map((model) => (
-
- {model.plan.toUpperCase()}
-
- }
- startContent={ }
- >
- {model.name}
-
- ))}
-
-
- }
-
-
-
- {agent.outputVisibility === "internal" && (
-
-
-
- Max calls from parent agent per turn
-
-
-
-
-
Max Calls Configuration
- This setting limits how many times a parent agent can call this agent in a single turn, to prevent infinite loops.
-
-
-
-
-
- handleUpdate({
- ...agent,
- maxCallsPerParentAgent: parseInt(e.target.value)
- })}
- className="w-full max-w-24"
- />
-
-
- )}
-
- {USE_TRANSFER_CONTROL_OPTIONS && (
-
-
- Conversation control after turn
-
- handleUpdate({
- ...agent,
- controlType: value as z.infer['controlType']
- })}
- />
-
- )}
-
- {useRag && (
-
-
-
- RAG DATA SOURCES
-
-
-
-
{
- const key = keys.currentKey as string;
- if (key) {
- handleUpdate({
- ...agent,
- ragDataSources: [...(agent.ragDataSources || []), key]
- });
- }
- }}
- startContent={ }
- >
- {dataSources
- .filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
- .length > 0 ? (
- dataSources
- .filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
- .map((ds) => (
-
- {ds.name}
-
- ))
- ) : (
-
-
-
-
-
-
- No data sources available
-
-
{
- e.preventDefault();
- e.stopPropagation();
- router.push(`/projects/${projectId}/sources`);
- }}
- startContent={ }
- >
- Go to RAG Sources
-
-
-
- )}
-
-
- {showRagCta && (
-
- Update Instructions
-
- )}
-
-
- )}
-
- {agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && (
-
- {(agent.ragDataSources || []).map((source) => {
- const ds = dataSources.find((ds) => ds._id === source);
- return (
-
-
-
-
-
- {ds?.name || "Unknown"}
-
-
- Data Source
-
-
-
-
{
- const newSources = agent.ragDataSources?.filter((s) => s !== source);
+
+ {/* Identity Section Card */}
+
+
+ Identity
+ >
+ }
+ labelWidth="md:w-32"
+ className="mb-1"
+ >
+
+
+
Name
+
+ {
+ setLocalName(value);
+ if (validateName(value)) {
handleUpdate({
...agent,
- ragDataSources: newSources
+ name: value
});
- }}
- startContent={ }
- >
- Remove
-
-
- );
- })}
+ }
+ }}
+ multiline={false}
+ showSaveButton={true}
+ showDiscardButton={true}
+ error={nameError}
+ className="w-full"
+ />
+
+
+
+
Description
+
+ handleUpdate({ ...agent, description: value })}
+ multiline={true}
+ placeholder="Enter a description for this agent"
+ className="w-full"
+ />
+
+
- )}
+
+ {/* Behavior Section Card */}
+
+
+ Behavior
+ >
+ }
+ labelWidth="md:w-32"
+ className="mb-1"
+ >
+
+
+
Agent Type
+
+ handleUpdate({
+ ...agent,
+ outputVisibility: value as z.infer["outputVisibility"]
+ })}
+ />
+
+
+
+
Model
+
+ {/* Model select/input logic unchanged */}
+ {eligibleModels === "*" && handleUpdate({
+ ...agent,
+ model: e.target.value as z.infer["model"]
+ })}
+ className="w-full max-w-64"
+ />}
+ {eligibleModels !== "*" && {
+ const key = keys.currentKey as string;
+ const model = eligibleModels.find((m) => m.name === key);
+ if (!model) {
+ return;
+ }
+ if (!model.eligible) {
+ setBillingError(`Please upgrade to the ${model.plan.toUpperCase()} plan to use this model.`);
+ return;
+ }
+ handleUpdate({
+ ...agent,
+ model: key as z.infer["model"]
+ });
+ }}
+ >
+
+ {eligibleModels.filter((model) => model.eligible).map((model) => (
+
+ {model.name}
+
+ ))}
+
+
+ {eligibleModels.filter((model) => !model.eligible).map((model) => (
+
+ {model.plan.toUpperCase()}
+
+ }
+ startContent={ }
+ >
+ {model.name}
+
+ ))}
+
+
+ }
+
+
+ {agent.outputVisibility === "internal" && (
+
+
Max Calls From Parent
+
+
{
+ setMaxCallsInput(e.target.value);
+ setMaxCallsError(null);
+ }}
+ onBlur={() => {
+ const num = Number(maxCallsInput);
+ if (!maxCallsInput || isNaN(num) || num < 1 || !Number.isInteger(num)) {
+ setMaxCallsError("Must be an integer >= 1");
+ return;
+ }
+ setMaxCallsError(null);
+ if (num !== agent.maxCallsPerParentAgent) {
+ handleUpdate({
+ ...agent,
+ maxCallsPerParentAgent: num
+ });
+ }
+ }}
+ className="w-full max-w-24"
+ />
+ {maxCallsError && (
+
{maxCallsError}
+ )}
+
+
+ )}
+ {USE_TRANSFER_CONTROL_OPTIONS && (
+
+
After Turn
+
+ handleUpdate({
+ ...agent,
+ controlType: value as z.infer["controlType"]
+ })}
+ />
+
+
+ )}
+
+
+ {/* RAG Data Sources Section Card */}
+
+
+ RAG
+ >
+ }
+ labelWidth="md:w-32"
+ className="mb-1"
+ >
+
+
+
Add Source
+
+
{
+ const key = keys.currentKey as string;
+ if (key) {
+ handleUpdate({
+ ...agent,
+ ragDataSources: [...(agent.ragDataSources || []), key]
+ });
+ }
+ }}
+ startContent={ }
+ >
+ {dataSources
+ .filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
+ .length > 0 ? (
+ dataSources
+ .filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
+ .map((ds) => (
+
+ {ds.name}
+
+ ))
+ ) : (
+
+
+
+
+
+
+ No data sources available
+
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ router.push(`/projects/${projectId}/sources`);
+ }}
+ startContent={ }
+ >
+ Go to RAG Sources
+
+
+
+ )}
+
+ {showRagCta && (
+
+ Update Instructions
+
+ )}
+
+
+ {agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && (
+
+ {(agent.ragDataSources || []).map((source) => {
+ const ds = dataSources.find((ds) => ds._id === source);
+ return (
+
+
+
+
+
+
+
+ {ds?.name || "Unknown"}
+
+
+ Data Source
+
+
+
+
{
+ const newSources = agent.ragDataSources?.filter((s) => s !== source);
+ handleUpdate({
+ ...agent,
+ ragDataSources: newSources
+ });
+ }}
+ startContent={ }
+ >
+ Remove
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* The rest of the configuration sections will be refactored in subsequent steps */}
)}
diff --git a/apps/rowboat/components/common/section-card.tsx b/apps/rowboat/components/common/section-card.tsx
new file mode 100644
index 00000000..350bf536
--- /dev/null
+++ b/apps/rowboat/components/common/section-card.tsx
@@ -0,0 +1,36 @@
+import React, { useState } from "react";
+import { ChevronDown, ChevronRight } from "lucide-react";
+
+interface SectionCardProps {
+ title: React.ReactNode;
+ children: React.ReactNode;
+ labelWidth?: string; // e.g., 'md:w-32'
+ className?: string;
+}
+
+export function SectionCard({ title, children, labelWidth = 'md:w-32', className = '' }: SectionCardProps) {
+ const [expanded, setExpanded] = useState(true);
+
+ return (
+
+
setExpanded((v) => !v)}
+ aria-expanded={expanded}
+ >
+ {expanded ? : }
+ {title}
+
+
+ {expanded && children}
+
+
+ );
+}