mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-28 09:56:23 +02:00
Add section cards inside agent config
This commit is contained in:
parent
0efd41a1f5
commit
e1f1120d92
3 changed files with 377 additions and 363 deletions
|
|
@ -193,19 +193,19 @@ export function EditableField({
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={20}
|
maxRows={20}
|
||||||
className="w-full"
|
className="w-full text-sm focus-visible:ring-0 focus:ring-0 outline-none"
|
||||||
classNames={{
|
classNames={{
|
||||||
...commonProps.classNames,
|
...commonProps.classNames,
|
||||||
input: "rounded-md py-2",
|
input: "rounded-md py-2 text-base focus-visible:ring-0 focus:ring-0 outline-none",
|
||||||
inputWrapper: "rounded-md border-medium py-1"
|
inputWrapper: "rounded-md border-medium py-1"
|
||||||
}}
|
}}
|
||||||
/>}
|
/>}
|
||||||
{!multiline && <Input
|
{!multiline && <Input
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
className="w-full"
|
className="w-full text-sm focus-visible:ring-0 focus:ring-0 outline-none"
|
||||||
classNames={{
|
classNames={{
|
||||||
...commonProps.classNames,
|
...commonProps.classNames,
|
||||||
input: clsx("rounded-md py-2", {
|
input: clsx("rounded-md py-2 text-base focus-visible:ring-0 focus:ring-0 outline-none", {
|
||||||
"border-0 focus:outline-none pl-2": inline
|
"border-0 focus:outline-none pl-2": inline
|
||||||
}),
|
}),
|
||||||
inputWrapper: clsx("rounded-md border-medium py-1", {
|
inputWrapper: clsx("rounded-md border-medium py-1", {
|
||||||
|
|
@ -236,8 +236,10 @@ export function EditableField({
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
"rounded-md border border-gray-200 dark:border-gray-700 px-2 py-1 min-h-[40px] text-sm",
|
||||||
{
|
{
|
||||||
"border border-gray-300 dark:border-gray-600 rounded px-3 py-3": !inline,
|
"whitespace-pre-wrap": multiline,
|
||||||
|
"flex items-center": !multiline,
|
||||||
"bg-transparent focus:outline-none focus:ring-0 border-0 rounded-none text-gray-900 dark:text-gray-100": inline,
|
"bg-transparent focus:outline-none focus:ring-0 border-0 rounded-none text-gray-900 dark:text-gray-100": inline,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { WithStringId } from "../../../lib/types/types";
|
||||||
import { WorkflowPrompt, WorkflowAgent, Workflow, WorkflowTool } from "../../../lib/types/workflow_types";
|
import { WorkflowPrompt, WorkflowAgent, Workflow, WorkflowTool } from "../../../lib/types/workflow_types";
|
||||||
import { DataSource } from "../../../lib/types/datasource_types";
|
import { DataSource } from "../../../lib/types/datasource_types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { PlusIcon, Sparkles, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon } from "lucide-react";
|
import { PlusIcon, Sparkles, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon, UserIcon, Settings } from "lucide-react";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { usePreviewModal } from "../workflow/preview-modal";
|
import { usePreviewModal } from "../workflow/preview-modal";
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection } from "@heroui/react";
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection } from "@heroui/react";
|
||||||
|
|
@ -24,6 +24,7 @@ import { useCopilot } from "../copilot/use-copilot";
|
||||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { SectionCard } from "@/components/common/section-card";
|
||||||
|
|
||||||
// Common section header styles
|
// Common section header styles
|
||||||
const sectionHeaderStyles = "block text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400";
|
const sectionHeaderStyles = "block text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400";
|
||||||
|
|
@ -175,11 +176,19 @@ export function AgentConfig({
|
||||||
currentAgentName: agent.name
|
currentAgentName: agent.name
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add local state for max calls input
|
||||||
|
const [maxCallsInput, setMaxCallsInput] = useState(String(agent.maxCallsPerParentAgent || 3));
|
||||||
|
const [maxCallsError, setMaxCallsError] = useState<string | null>(null);
|
||||||
|
// Sync local state with agent prop
|
||||||
|
useEffect(() => {
|
||||||
|
setMaxCallsInput(String(agent.maxCallsPerParentAgent || 3));
|
||||||
|
}, [agent.maxCallsPerParentAgent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{agent.name}
|
{agent.name}
|
||||||
</div>
|
</div>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
|
|
@ -202,7 +211,7 @@ export function AgentConfig({
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-4 py-2 text-sm font-medium transition-colors relative",
|
"px-4 py-2 text-base font-semibold transition-colors relative",
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? "text-indigo-600 dark:text-indigo-400 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-indigo-500 dark:after:bg-indigo-400"
|
? "text-indigo-600 dark:text-indigo-400 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-indigo-500 dark:after:bg-indigo-400"
|
||||||
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||||
|
|
@ -389,365 +398,332 @@ export function AgentConfig({
|
||||||
|
|
||||||
|
|
||||||
{activeTab === 'configurations' && (
|
{activeTab === 'configurations' && (
|
||||||
<div className="space-y-8 pb-6">
|
<div className="flex flex-col gap-4 pb-4 pt-0">
|
||||||
{!agent.locked && (
|
{/* Identity Section Card */}
|
||||||
<div className="space-y-2">
|
<SectionCard
|
||||||
<label className={sectionHeaderStyles}>
|
title={
|
||||||
Name
|
<>
|
||||||
</label>
|
<UserIcon className="w-5 h-5 text-indigo-500" />
|
||||||
<div className={clsx(
|
<span className="text-base font-semibold">Identity</span>
|
||||||
"border rounded-lg focus-within:ring-2",
|
</>
|
||||||
nameError
|
}
|
||||||
? "border-red-500 focus-within:ring-red-500/20"
|
labelWidth="md:w-32"
|
||||||
: "border-gray-200 dark:border-gray-700 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
|
className="mb-1"
|
||||||
)}>
|
>
|
||||||
<Textarea
|
<div className="flex flex-col gap-6">
|
||||||
value={agent.name}
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
useValidation={true}
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Name</label>
|
||||||
updateOnBlur={true}
|
<div className="flex-1">
|
||||||
validate={(value) => {
|
<EditableField
|
||||||
const error = validateAgentName(value, agent.name, usedAgentNames);
|
value={localName}
|
||||||
setNameError(error);
|
onChange={(value) => {
|
||||||
return { valid: !error, errorMessage: error || undefined };
|
setLocalName(value);
|
||||||
}}
|
if (validateName(value)) {
|
||||||
onValidatedChange={(value) => {
|
|
||||||
handleUpdate({
|
|
||||||
...agent,
|
|
||||||
name: value
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
placeholder="Enter agent name..."
|
|
||||||
className="w-full text-sm bg-transparent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-4 py-3"
|
|
||||||
autoResize
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{nameError && (
|
|
||||||
<p className="text-sm text-red-500">{nameError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className={sectionHeaderStyles}>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
value={agent.description || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
handleUpdate({
|
|
||||||
...agent,
|
|
||||||
description: e.target.value
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
placeholder="Enter a description for this agent"
|
|
||||||
className={textareaStyles}
|
|
||||||
autoResize
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<label className={sectionHeaderStyles}>
|
|
||||||
Agent Type
|
|
||||||
</label>
|
|
||||||
<div className="relative ml-2 group">
|
|
||||||
<Info
|
|
||||||
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer transition-colors"
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-full left-0 mb-2 p-3 w-80 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs invisible group-hover:visible z-50">
|
|
||||||
<div className="mb-1 font-medium">Agent Types</div>
|
|
||||||
Conversation agents' responses are user-facing. You can use conversation agents for multi-turn conversations with users.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
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.
|
|
||||||
<div className="absolute h-2 w-2 bg-white dark:bg-gray-800 transform rotate-45 -bottom-1 left-4 border-r border-b border-gray-200 dark:border-gray-700"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<CustomDropdown
|
|
||||||
value={agent.outputVisibility}
|
|
||||||
options={[
|
|
||||||
{ key: "user_facing", label: "Conversation Agent" },
|
|
||||||
{ key: "internal", label: "Task Agent" }
|
|
||||||
]}
|
|
||||||
onChange={(value) => handleUpdate({
|
|
||||||
...agent,
|
|
||||||
outputVisibility: value as z.infer<typeof WorkflowAgent>['outputVisibility']
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<label className={sectionHeaderStyles}>
|
|
||||||
Model
|
|
||||||
</label>
|
|
||||||
{eligibleModels === "*" && <div className="relative ml-2 group">
|
|
||||||
<Info
|
|
||||||
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer transition-colors"
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-full left-0 mb-2 p-3 w-80 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs invisible group-hover:visible z-50">
|
|
||||||
<div className="mb-1 font-medium">Model Configuration</div>
|
|
||||||
Set this according to the PROVIDER_BASE_URL you have set in your .env file (such as your LiteLLM, gateway).
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
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.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
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.
|
|
||||||
<div className="absolute h-2 w-2 bg-white dark:bg-gray-800 transform rotate-45 -bottom-1 left-4 border-r border-b border-gray-200 dark:border-gray-700"></div>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
{eligibleModels === "*" && <Input
|
|
||||||
value={agent.model}
|
|
||||||
onChange={(e) => handleUpdate({
|
|
||||||
...agent,
|
|
||||||
model: e.target.value as z.infer<typeof WorkflowAgent>['model']
|
|
||||||
})}
|
|
||||||
className="w-full max-w-64"
|
|
||||||
/>}
|
|
||||||
{eligibleModels !== "*" && <Select
|
|
||||||
variant="bordered"
|
|
||||||
placeholder="Select model"
|
|
||||||
className="w-full max-w-64"
|
|
||||||
selectedKeys={[agent.model]}
|
|
||||||
onSelectionChange={(keys) => {
|
|
||||||
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<typeof WorkflowAgent>['model']
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectSection title="Available">
|
|
||||||
{eligibleModels.filter((model) => model.eligible).map((model) => (
|
|
||||||
<SelectItem
|
|
||||||
key={model.name}
|
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectSection>
|
|
||||||
<SelectSection title="Requires plan upgrade">
|
|
||||||
{eligibleModels.filter((model) => !model.eligible).map((model) => (
|
|
||||||
<SelectItem
|
|
||||||
key={model.name}
|
|
||||||
endContent={<Chip
|
|
||||||
color="warning"
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
{model.plan.toUpperCase()}
|
|
||||||
</Chip>
|
|
||||||
}
|
|
||||||
startContent={<StarIcon className="w-4 h-4 text-warning" />}
|
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectSection>
|
|
||||||
</Select>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{agent.outputVisibility === "internal" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<label className={sectionHeaderStyles}>
|
|
||||||
Max calls from parent agent per turn
|
|
||||||
</label>
|
|
||||||
<div className="relative ml-2 group">
|
|
||||||
<Info
|
|
||||||
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer transition-colors"
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-full left-0 mb-2 p-3 w-80 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs invisible group-hover:visible z-50">
|
|
||||||
<div className="mb-1 font-medium">Max Calls Configuration</div>
|
|
||||||
This setting limits how many times a parent agent can call this agent in a single turn, to prevent infinite loops.
|
|
||||||
<div className="absolute h-2 w-2 bg-white dark:bg-gray-800 transform rotate-45 -bottom-1 left-4 border-r border-b border-gray-200 dark:border-gray-700"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={agent.maxCallsPerParentAgent || 3}
|
|
||||||
onChange={(e) => handleUpdate({
|
|
||||||
...agent,
|
|
||||||
maxCallsPerParentAgent: parseInt(e.target.value)
|
|
||||||
})}
|
|
||||||
className="w-full max-w-24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{USE_TRANSFER_CONTROL_OPTIONS && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className={sectionHeaderStyles}>
|
|
||||||
Conversation control after turn
|
|
||||||
</label>
|
|
||||||
<CustomDropdown
|
|
||||||
value={agent.controlType}
|
|
||||||
options={
|
|
||||||
agent.outputVisibility === "internal"
|
|
||||||
? [
|
|
||||||
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
|
|
||||||
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{ key: "retain", label: "Retain control" },
|
|
||||||
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
|
|
||||||
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
onChange={(value) => handleUpdate({
|
|
||||||
...agent,
|
|
||||||
controlType: value as z.infer<typeof WorkflowAgent>['controlType']
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{useRag && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<label className={sectionHeaderStyles}>
|
|
||||||
RAG DATA SOURCES
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Select
|
|
||||||
variant="bordered"
|
|
||||||
placeholder="Add data source"
|
|
||||||
size="sm"
|
|
||||||
className="w-64"
|
|
||||||
onSelectionChange={(keys) => {
|
|
||||||
const key = keys.currentKey as string;
|
|
||||||
if (key) {
|
|
||||||
handleUpdate({
|
|
||||||
...agent,
|
|
||||||
ragDataSources: [...(agent.ragDataSources || []), key]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
startContent={<PlusIcon className="w-4 h-4 text-gray-500" />}
|
|
||||||
>
|
|
||||||
{dataSources
|
|
||||||
.filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
|
|
||||||
.length > 0 ? (
|
|
||||||
dataSources
|
|
||||||
.filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
|
|
||||||
.map((ds) => (
|
|
||||||
<SelectItem key={ds._id}>
|
|
||||||
{ds.name}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<SelectItem key="empty" isReadOnly>
|
|
||||||
<div className="flex flex-col items-center justify-center p-4 text-center">
|
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-700 mb-2">
|
|
||||||
<DatabaseIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
|
||||||
No data sources available
|
|
||||||
</div>
|
|
||||||
<CustomButton
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
router.push(`/projects/${projectId}/sources`);
|
|
||||||
}}
|
|
||||||
startContent={<DatabaseIcon className="w-3 h-3" />}
|
|
||||||
>
|
|
||||||
Go to RAG Sources
|
|
||||||
</CustomButton>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{showRagCta && (
|
|
||||||
<CustomButton
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleUpdateInstructions}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Update Instructions
|
|
||||||
</CustomButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{(agent.ragDataSources || []).map((source) => {
|
|
||||||
const ds = dataSources.find((ds) => ds._id === source);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={source}
|
|
||||||
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-indigo-50 dark:bg-indigo-900/20">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-indigo-600 dark:text-indigo-400"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{ds?.name || "Unknown"}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
Data Source
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<CustomButton
|
|
||||||
variant="tertiary"
|
|
||||||
size="sm"
|
|
||||||
className="text-gray-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
||||||
onClick={() => {
|
|
||||||
const newSources = agent.ragDataSources?.filter((s) => s !== source);
|
|
||||||
handleUpdate({
|
handleUpdate({
|
||||||
...agent,
|
...agent,
|
||||||
ragDataSources: newSources
|
name: value
|
||||||
});
|
});
|
||||||
}}
|
}
|
||||||
startContent={<Trash2 className="w-4 h-4" />}
|
}}
|
||||||
>
|
multiline={false}
|
||||||
Remove
|
showSaveButton={true}
|
||||||
</CustomButton>
|
showDiscardButton={true}
|
||||||
</div>
|
error={nameError}
|
||||||
);
|
className="w-full"
|
||||||
})}
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
|
||||||
|
<div className="flex-1">
|
||||||
|
<EditableField
|
||||||
|
value={agent.description || ""}
|
||||||
|
onChange={(value) => handleUpdate({ ...agent, description: value })}
|
||||||
|
multiline={true}
|
||||||
|
placeholder="Enter a description for this agent"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</SectionCard>
|
||||||
|
{/* Behavior Section Card */}
|
||||||
|
<SectionCard
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<Settings className="w-5 h-5 text-indigo-500" />
|
||||||
|
<span className="text-base font-semibold">Behavior</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
labelWidth="md:w-32"
|
||||||
|
className="mb-1"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Agent Type</label>
|
||||||
|
<div className="flex-1">
|
||||||
|
<CustomDropdown
|
||||||
|
value={agent.outputVisibility}
|
||||||
|
options={[
|
||||||
|
{ key: "user_facing", label: "Conversation Agent" },
|
||||||
|
{ key: "internal", label: "Task Agent" }
|
||||||
|
]}
|
||||||
|
onChange={(value) => handleUpdate({
|
||||||
|
...agent,
|
||||||
|
outputVisibility: value as z.infer<typeof WorkflowAgent>["outputVisibility"]
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Model</label>
|
||||||
|
<div className="flex-1">
|
||||||
|
{/* Model select/input logic unchanged */}
|
||||||
|
{eligibleModels === "*" && <Input
|
||||||
|
value={agent.model}
|
||||||
|
onChange={(e) => handleUpdate({
|
||||||
|
...agent,
|
||||||
|
model: e.target.value as z.infer<typeof WorkflowAgent>["model"]
|
||||||
|
})}
|
||||||
|
className="w-full max-w-64"
|
||||||
|
/>}
|
||||||
|
{eligibleModels !== "*" && <Select
|
||||||
|
variant="bordered"
|
||||||
|
placeholder="Select model"
|
||||||
|
className="w-full max-w-64"
|
||||||
|
selectedKeys={[agent.model]}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
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<typeof WorkflowAgent>["model"]
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectSection title="Available">
|
||||||
|
{eligibleModels.filter((model) => model.eligible).map((model) => (
|
||||||
|
<SelectItem
|
||||||
|
key={model.name}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectSection>
|
||||||
|
<SelectSection title="Requires plan upgrade">
|
||||||
|
{eligibleModels.filter((model) => !model.eligible).map((model) => (
|
||||||
|
<SelectItem
|
||||||
|
key={model.name}
|
||||||
|
endContent={<Chip
|
||||||
|
color="warning"
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
>
|
||||||
|
{model.plan.toUpperCase()}
|
||||||
|
</Chip>
|
||||||
|
}
|
||||||
|
startContent={<StarIcon className="w-4 h-4 text-warning" />}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectSection>
|
||||||
|
</Select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{agent.outputVisibility === "internal" && (
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Max Calls From Parent</label>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={maxCallsInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 && (
|
||||||
|
<p className="text-sm text-red-500 mt-1">{maxCallsError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{USE_TRANSFER_CONTROL_OPTIONS && (
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">After Turn</label>
|
||||||
|
<div className="flex-1">
|
||||||
|
<CustomDropdown
|
||||||
|
value={agent.controlType}
|
||||||
|
options={
|
||||||
|
agent.outputVisibility === "internal"
|
||||||
|
? [
|
||||||
|
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
|
||||||
|
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ key: "retain", label: "Retain control" },
|
||||||
|
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
|
||||||
|
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onChange={(value) => handleUpdate({
|
||||||
|
...agent,
|
||||||
|
controlType: value as z.infer<typeof WorkflowAgent>["controlType"]
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
{/* RAG Data Sources Section Card */}
|
||||||
|
<SectionCard
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<DatabaseIcon className="w-5 h-5 text-indigo-500" />
|
||||||
|
<span className="text-base font-semibold">RAG</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
labelWidth="md:w-32"
|
||||||
|
className="mb-1"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Add Source</label>
|
||||||
|
<div className="flex-1 flex items-center gap-3">
|
||||||
|
<Select
|
||||||
|
variant="bordered"
|
||||||
|
placeholder="Add data source"
|
||||||
|
size="sm"
|
||||||
|
className="w-64"
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const key = keys.currentKey as string;
|
||||||
|
if (key) {
|
||||||
|
handleUpdate({
|
||||||
|
...agent,
|
||||||
|
ragDataSources: [...(agent.ragDataSources || []), key]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startContent={<PlusIcon className="w-4 h-4 text-gray-500" />}
|
||||||
|
>
|
||||||
|
{dataSources
|
||||||
|
.filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
|
||||||
|
.length > 0 ? (
|
||||||
|
dataSources
|
||||||
|
.filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
|
||||||
|
.map((ds) => (
|
||||||
|
<SelectItem key={ds._id}>
|
||||||
|
{ds.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem key="empty" isReadOnly>
|
||||||
|
<div className="flex flex-col items-center justify-center p-4 text-center">
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-700 mb-2">
|
||||||
|
<DatabaseIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
|
No data sources available
|
||||||
|
</div>
|
||||||
|
<CustomButton
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
router.push(`/projects/${projectId}/sources`);
|
||||||
|
}}
|
||||||
|
startContent={<DatabaseIcon className="w-3 h-3" />}
|
||||||
|
>
|
||||||
|
Go to RAG Sources
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
{showRagCta && (
|
||||||
|
<CustomButton
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleUpdateInstructions}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Update Instructions
|
||||||
|
</CustomButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-2 mt-2">
|
||||||
|
{(agent.ragDataSources || []).map((source) => {
|
||||||
|
const ds = dataSources.find((ds) => ds._id === source);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={source}
|
||||||
|
className="flex items-center justify-between p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-indigo-50 dark:bg-indigo-900/20">
|
||||||
|
<DatabaseIcon className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{ds?.name || "Unknown"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Data Source
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CustomButton
|
||||||
|
variant="tertiary"
|
||||||
|
size="sm"
|
||||||
|
className="text-gray-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||||
|
onClick={() => {
|
||||||
|
const newSources = agent.ragDataSources?.filter((s) => s !== source);
|
||||||
|
handleUpdate({
|
||||||
|
...agent,
|
||||||
|
ragDataSources: newSources
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
startContent={<Trash2 className="w-4 h-4" />}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
{/* The rest of the configuration sections will be refactored in subsequent steps */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
36
apps/rowboat/components/common/section-card.tsx
Normal file
36
apps/rowboat/components/common/section-card.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className={`rounded-lg shadow border border-zinc-200 dark:border-zinc-800 p-6 bg-white dark:bg-gray-900 ${className}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-2 ${labelWidth} ${expanded ? 'mb-6' : 'mb-1'} focus:outline-none select-none`}
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
aria-expanded={expanded}
|
||||||
|
>
|
||||||
|
{expanded ? <ChevronDown className="w-4 h-4 text-gray-400" /> : <ChevronRight className="w-4 h-4 text-gray-400" />}
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: expanded ? 9999 : 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
transition: "max-height 0.2s cubic-bezier(0.4,0,0.2,1)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded && children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue