ui updates on workflow editor

This commit is contained in:
ramnique 2025-01-20 12:35:17 +05:30
parent b6728d270d
commit 988dca96dc
11 changed files with 478 additions and 398 deletions

View file

@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
import { useClickAway } from "@/hooks/use-click-away";
import MarkdownContent from "@/app/lib/components/markdown-content";
import clsx from "clsx";
import { Label } from "@/app/lib/components/label";
interface EditableFieldProps {
value: string;
@ -86,7 +87,7 @@ export function EditableField({
return (
<div ref={ref} className={clsx("flex flex-col gap-1", className)}>
{(label || isEditing && multiline) && <div className="flex items-center gap-2 justify-between">
{label && <div className="block text-sm font-medium text-foreground-500 pb-1.5">{label}</div>}
{label && <Label label={label} />}
{isEditing && multiline && <div className="flex items-center gap-2">
<Button
size="sm"

View file

@ -0,0 +1,3 @@
export function Label({ label }: { label: string }) {
return <div className="text-xs font-medium text-gray-400 uppercase">{label}</div>;
}

View file

@ -7,19 +7,19 @@ export default function MarkdownContent({ content }: { content: string }) {
remarkPlugins={[remarkGfm]}
components={{
h1({ children }) {
return <h1 className="text-2xl font-bold py-2">{children}</h1>
return <h1 className="text-xl font-bold py-2">{children}</h1>
},
h2({ children }) {
return <h2 className="text-xl font-bold py-2">{children}</h2>
return <h2 className="text-lg font-bold py-2">{children}</h2>
},
h3({ children }) {
return <h3 className="text-lg font-semibold py-2">{children}</h3>
return <h3 className="text-base font-semibold py-2">{children}</h3>
},
h4({ children }) {
return <h4 className="text-base font-semibold py-2">{children}</h4>
return <h4 className="text-sm font-semibold py-2">{children}</h4>
},
h5({ children }) {
return <h5 className="text-sm font-semibold py-2">{children}</h5>
return <h5 className="text-xs font-semibold py-2">{children}</h5>
},
h6({ children }) {
return <h6 className="text-xs font-semibold py-2">{children}</h6>

View file

@ -159,7 +159,10 @@ export const WorkflowAgent = z.object({
examples: z.string().optional(),
prompts: z.array(z.string()),
tools: z.array(z.string()),
model: z.string(),
model: z.union([
z.literal('gpt-4o'),
z.literal('gpt-4o-mini'),
]),
locked: z.boolean().default(false).describe('Whether this agent is locked and cannot be deleted').optional(),
toggleAble: z.boolean().default(true).describe('Whether this agent can be enabled or disabled').optional(),
global: z.boolean().default(false).describe('Whether this agent is a global agent, in which case it cannot be connected to other agents').optional(),

View file

@ -8,13 +8,14 @@ import MarkdownContent from "@/app/lib/components/markdown-content";
import Link from "next/link";
import { apiV1 } from "rowboat-shared";
import { EditableField } from "@/app/lib/components/editable-field";
import { MessageSquareIcon, EllipsisIcon } from "lucide-react";
function UserMessage({ content }: { content: string }) {
return <div className="self-end ml-[30%] flex flex-col">
<div className="text-right text-gray-500 text-sm mr-3">
<div className="text-right text-gray-500 text-xs mr-3">
User
</div>
<div className="bg-gray-100 px-3 py-1 rounded-lg rounded-br-none">
<div className="bg-gray-100 px-3 py-1 rounded-lg rounded-br-none text-sm">
<MarkdownContent content={content} />
</div>
</div>;
@ -26,17 +27,13 @@ function InternalAssistantMessage({ content, sender, latency }: { content: strin
// show a message icon with a + symbol to expand and show the content
return <div className="self-start mr-[30%]">
{!expanded && <button className="flex items-center text-gray-400 hover:text-gray-600 gap-1 group" onClick={() => setExpanded(true)}>
<svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M16 10.5h.01m-4.01 0h.01M8 10.5h.01M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-6.6a1 1 0 0 0-.69.275l-2.866 2.723A.5.5 0 0 1 8 18.635V17a1 1 0 0 0-1-1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z" />
</svg>
<svg className="group-hover:hidden w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeWidth="2" d="M6 12h.01m6 0h.01m5.99 0h.01" />
</svg>
<MessageSquareIcon size={16} />
<EllipsisIcon size={16} />
<span className="hidden group-hover:block text-xs">Show debug message</span>
</button>}
{expanded && <div className="flex flex-col">
<div className="flex gap-2 justify-between items-center">
<div className="text-gray-500 text-sm pl-3">
<div className="text-gray-500 text-xs pl-3">
{sender ?? 'Assistant'}
</div>
<button className="flex items-center gap-1 text-gray-400 hover:text-gray-600" onClick={() => setExpanded(false)}>
@ -55,27 +52,25 @@ function InternalAssistantMessage({ content, sender, latency }: { content: strin
function AssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
return <div className="self-start mr-[30%] flex flex-col">
<div className="flex gap-2 justify-between items-center">
<div className="text-gray-500 text-sm pl-3">
<div className="text-gray-500 text-xs pl-3">
{sender ?? 'Assistant'}
</div>
<div className="text-gray-400 text-xs pr-3">
{Math.round(latency / 1000)}s
</div>
</div>
<div className="bg-gray-100 px-3 py-1 rounded-lg rounded-bl-none">
<div className="bg-gray-100 px-3 py-1 rounded-lg rounded-bl-none text-sm">
<MarkdownContent content={content} />
</div>
</div>;
}
function AssistantMessageLoading() {
return <div className="self-start mr-[30%] flex flex-col">
<div className="text-gray-500 text-sm ml-3">
return <div className="self-start mr-[30%] flex flex-col text-gray-500 items-start">
<div className="text-gray-500 text-xs ml-3">
Assistant
</div>
<div className="bg-gray-100 p-3 rounded-lg rounded-bl-none animate-pulse w-20">
<Spinner />
</div>
<Spinner size="sm" className="mt-2 ml-3" />
</div>;
}
@ -84,8 +79,8 @@ function UserMessageLoading() {
<div className="text-right text-gray-500 text-sm mr-3">
User
</div>
<div className="bg-gray-100 p-3 rounded-lg rounded-br-none animate-pulse w-20">
<Spinner />
<div className="bg-gray-100 p-3 rounded-lg rounded-br-none animate-pulse w-20 text-gray-800">
<Spinner size="sm" />
</div>
</div>;
}

View file

@ -1,11 +1,13 @@
"use client";
import { AgenticAPITool, DataSource, WithStringId, WorkflowAgent, WorkflowPrompt } from "@/app/lib/types";
import { Accordion, AccordionItem, Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Radio, RadioGroup, Select, SelectItem, Textarea } from "@nextui-org/react";
import { Button, Divider, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Radio, RadioGroup, Select, SelectItem } from "@nextui-org/react";
import { z } from "zod";
import { DataSourceIcon } from "@/app/lib/components/datasource-icon";
import { ActionButton, Pane } from "./pane";
import { EditableField } from "@/app/lib/components/editable-field";
import MarkdownContent from "@/app/lib/components/markdown-content";
import { Label } from "@/app/lib/components/label";
import { PlusIcon, XIcon } from "lucide-react";
import { List } from "./config_list";
export function AgentConfig({
agent,
@ -38,7 +40,7 @@ export function AgentConfig({
</ActionButton>
]}>
<div className="flex flex-col gap-4">
{!agent.locked && (
{!agent.locked && <>
<EditableField
key="name"
label="Name"
@ -60,7 +62,8 @@ export function AgentConfig({
return { valid: true };
}}
/>
)}
<Divider />
</>}
<EditableField
key="description"
@ -75,6 +78,8 @@ export function AgentConfig({
placeholder="Enter a description for this agent"
/>
<Divider />
<div className="w-full flex flex-col">
<EditableField
key="instructions"
@ -90,6 +95,9 @@ export function AgentConfig({
multiline
/>
</div>
<Divider />
<div className="w-full flex flex-col">
<EditableField
key="examples"
@ -107,37 +115,29 @@ export function AgentConfig({
/>
</div>
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">Attach prompts:</div>
<div className="flex gap-4 flex-wrap">
{agent.prompts.map((prompt) => (
<div key={prompt} className="bg-gray-100 border-1 border-gray-200 shadow-sm rounded-lg px-2 py-1 flex items-center gap-2">
<div>{prompt}</div>
<button
onClick={() => {
const newPrompts = agent.prompts.filter((p) => p !== prompt);
handleUpdate({
...agent,
prompts: newPrompts
});
}}
className="bg-white rounded-md text-gray-500 hover:text-gray-800"
>
<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M6 18 17.94 6M18 18 6.06 6" />
</svg>
</button>
</div>
))}
</div>
<Dropdown>
<Divider />
<div className="flex flex-col gap-4 items-start">
<Label label="Prompts" />
<List
items={agent.prompts.map((prompt) => ({
id: prompt,
node: <div>{prompt}</div>
}))}
onRemove={(id) => {
const newPrompts = agent.prompts.filter((p) => p !== id);
handleUpdate({
...agent,
prompts: newPrompts
});
}}
/>
<Dropdown size="sm">
<DropdownTrigger>
<Button
variant="bordered"
variant="light"
size="sm"
startContent={<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
startContent={<PlusIcon size={16} />}
>
Add prompt
</Button>
@ -154,40 +154,33 @@ export function AgentConfig({
</DropdownMenu>
</Dropdown>
</div>
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">RAG:</div>
<div className="flex gap-4 flex-wrap">
{agent.ragDataSources?.map((source) => (
<div key={source} className="bg-gray-100 border-1 border-gray-200 shadow-sm rounded-lg px-2 py-1 flex items-center gap-2">
<div className="flex items-center gap-1">
<DataSourceIcon type={dataSources.find((ds) => ds._id === source)?.data.type} />
<div>{dataSources.find((ds) => ds._id === source)?.name || "Unknown"}</div>
</div>
<button
onClick={() => {
const newSources = agent.ragDataSources?.filter((s) => s !== source);
handleUpdate({
...agent,
ragDataSources: newSources
});
}}
className="bg-white rounded-md text-gray-500 hover:text-gray-800"
>
<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M6 18 17.94 6M18 18 6.06 6" />
</svg>
</button>
<Divider />
<div className="flex flex-col gap-4 items-start">
<Label label="RAG" />
<List
items={agent.ragDataSources?.map((source) => ({
id: source,
node: <div className="flex items-center gap-1">
<DataSourceIcon type={dataSources.find((ds) => ds._id === source)?.data.type} />
<div>{dataSources.find((ds) => ds._id === source)?.name || "Unknown"}</div>
</div>
))}
</div>
})) || []}
onRemove={(id) => {
const newSources = agent.ragDataSources?.filter((s) => s !== id);
handleUpdate({
...agent,
ragDataSources: newSources
});
}}
/>
<Dropdown>
<DropdownTrigger>
<Button
variant="bordered"
variant="light"
size="sm"
startContent={<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
startContent={<PlusIcon size={16} />}
>
Add data source
</Button>
@ -206,72 +199,64 @@ export function AgentConfig({
))}
</DropdownMenu>
</Dropdown>
{agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && <Accordion>
<AccordionItem
key="rag"
isCompact
aria-label="Advanced RAG configuration"
title="Advanced RAG configuration"
>
<div className="flex flex-col gap-4">
<RadioGroup
label="Return type:"
orientation="horizontal"
value={agent.ragReturnType}
onValueChange={(value) => handleUpdate({
...agent,
ragReturnType: value as z.infer<typeof WorkflowAgent>['ragReturnType']
})}
>
<Radio value="chunks">Chunks</Radio>
<Radio value="content">Content</Radio>
</RadioGroup>
<Input
label="No. of matches:"
labelPlacement="outside"
variant="bordered"
value={agent.ragK.toString()}
onValueChange={(value) => handleUpdate({
...agent,
ragK: parseInt(value)
})}
type="number"
/>
</div>
</AccordionItem>
</Accordion>}
</div>
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">Tools:</div>
<div className="flex gap-4 flex-wrap">
{agent.tools.map((tool) => (
<div key={tool} className="bg-gray-100 border-1 border-gray-200 shadow-sm rounded-lg px-2 py-1 flex items-center gap-2">
<div className="font-mono">{tool}</div>
<button
onClick={() => {
const newTools = agent.tools.filter((t) => t !== tool);
handleUpdate({
...agent,
tools: newTools
});
}}
className="bg-white rounded-md text-gray-500 hover:text-gray-800"
>
<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M6 18 17.94 6M18 18 6.06 6" />
</svg>
</button>
</div>
))}
<Divider />
{agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && <>
<Label label="Advanced RAG configuration" />
<div className="ml-4 flex flex-col gap-4">
<Label label="Return type" />
<RadioGroup
size="sm"
orientation="horizontal"
value={agent.ragReturnType}
onValueChange={(value) => handleUpdate({
...agent,
ragReturnType: value as z.infer<typeof WorkflowAgent>['ragReturnType']
})}
>
<Radio value="chunks">Chunks</Radio>
<Radio value="content">Content</Radio>
</RadioGroup>
<Label label="No. of matches" />
<Input
variant="bordered"
size="sm"
className="w-20"
value={agent.ragK.toString()}
onValueChange={(value) => handleUpdate({
...agent,
ragK: parseInt(value)
})}
type="number"
/>
</div>
<Divider />
</>}
<div className="flex flex-col gap-4 items-start">
<Label label="Tools" />
<List
items={agent.tools.map((tool) => ({
id: tool,
node: <div>{tool}</div>
}))}
onRemove={(id) => {
const newTools = agent.tools.filter((t) => t !== id);
handleUpdate({
...agent,
tools: newTools
});
}}
/>
<Dropdown>
<DropdownTrigger>
<Button
variant="bordered"
variant="light"
size="sm"
startContent={<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
startContent={<PlusIcon size={16} />}
>
Add tool
</Button>
@ -288,37 +273,29 @@ export function AgentConfig({
</DropdownMenu>
</Dropdown>
</div>
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">Connected agents:</div>
<div className="flex gap-4 flex-wrap">
{agent.connectedAgents?.map((connectedAgentName) => (
<div key={connectedAgentName} className="bg-gray-100 border-1 border-gray-200 shadow-sm rounded-lg px-2 py-1 flex items-center gap-2">
<div>{connectedAgentName}</div>
<button
onClick={() => {
const newAgents = (agent.connectedAgents || []).filter((a) => a !== connectedAgentName);
handleUpdate({
...agent,
connectedAgents: newAgents
});
}}
className="bg-white rounded-md text-gray-500 hover:text-gray-800"
>
<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M6 18 17.94 6M18 18 6.06 6" />
</svg>
</button>
</div>
))}
</div>
<Divider />
<div className="flex flex-col gap-4 items-start">
<Label label="Connected agents" />
<List
items={agent.connectedAgents?.map((connectedAgentName) => ({
id: connectedAgentName,
node: <div>{connectedAgentName}</div>
})) || []}
onRemove={(id) => {
const newAgents = (agent.connectedAgents || []).filter((a) => a !== id);
handleUpdate({
...agent,
connectedAgents: newAgents
});
}}
/>
<Dropdown>
<DropdownTrigger>
<Button
variant="bordered"
variant="light"
size="sm"
startContent={<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
startContent={<PlusIcon size={16} />}
>
Connect agent
</Button>
@ -339,27 +316,30 @@ export function AgentConfig({
</DropdownMenu>
</Dropdown>
</div>
<Divider />
<div className="flex flex-col gap-2 items-start">
<EditableField
label="Model:"
value={agent.model}
onChange={(value) => {
handleUpdate({
...agent,
model: value
});
}}
validate={(value) => {
if (value.length === 0) {
return { valid: false, errorMessage: "Model cannot be empty" };
}
return { valid: true };
}}
<Label label="Model" />
<Select
variant="bordered"
selectedKeys={[agent.model]}
size="sm"
onSelectionChange={(keys) => handleUpdate({
...agent,
model: keys.currentKey! as z.infer<typeof WorkflowAgent>['model']
})}
className="w-40"
/>
>
{WorkflowAgent.shape.model.options.map((model) => (
<SelectItem key={model.value} value={model.value}>{model.value}</SelectItem>
))}
</Select>
</div>
<Divider />
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">Conversation control after turn:</div>
<Label label="Conversation control after turn" />
<Select
variant="bordered"
selectedKeys={[agent.controlType]}

View file

@ -0,0 +1,38 @@
import { XIcon } from "lucide-react";
export function List({
items,
onRemove,
}: {
items: {
id: string;
node: React.ReactNode;
}[];
onRemove: (id: string) => void;
}) {
return <div className="ml-4 flex flex-col gap-2 items-start">
{items.map((item) => (
<ListItem key={item.id} onRemove={() => onRemove(item.id)}>
{item.node}
</ListItem>
))}
</div>;
}
export function ListItem({
children,
onRemove,
}: {
children: React.ReactNode;
onRemove: () => void;
}) {
return <div className="flex items-center gap-2">
<div className="bg-gray-400 rounded-full w-1 h-1"></div>
<div className="flex items-center gap-2 bg-gray-100 rounded-md px-2 py-1 group">
<div className="grow text-sm">{children}</div>
<button onClick={onRemove} className="hidden rounded-md hover:bg-gray-500 text-gray-500 hover:text-white group-hover:block">
<XIcon size={16} />
</button>
</div>
</div>
}

View file

@ -130,10 +130,10 @@ function AssistantMessage({
return <div className="flex flex-col gap-2 mb-8">
<RawJsonResponse message={message} />
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
{message.content.response.map((part, index) => {
if (part.type === "text") {
return <div key={index}>
return <div key={index} className="text-sm">
<MarkdownContent content={part.content} />
</div>;
} else if (part.type === "action") {
@ -158,7 +158,7 @@ function UserMessage({
}: {
message: z.infer<typeof CopilotUserMessage>;
}) {
return <div className="bg-gray-50 border border-gray-200 rounded-sm px-2">
return <div className="bg-gray-50 border border-gray-200 rounded-sm px-2 text-sm">
<MarkdownContent content={message.content} />
</div>
}
@ -376,7 +376,7 @@ function App({
return <div className="h-full flex flex-col">
<CopilotContext.Provider value={{ workflow, handleApplyChange, appliedChanges }}>
<div className="grow flex flex-col gap-2 overflow-auto px-2">
<div className="grow flex flex-col gap-2 overflow-auto px-1">
{messages.map((m, index) => {
// Calculate if this assistant message is stale
const isStale = m.role === 'assistant' && messages.slice(index + 1).some(
@ -402,7 +402,7 @@ function App({
)}
</>;
})}
{loadingResponse && <div className="p-2 flex items-center animate-pulse text-gray-600">
{loadingResponse && <div className="px-2 py-1 flex items-center animate-pulse text-gray-600 text-xs">
<div>
{loadingMessage}
</div>

View file

@ -39,23 +39,24 @@ export function Pane({
export function ActionButton({
icon = null,
children,
onClick,
onClick = undefined,
disabled = false,
primary = false,
}: {
icon?: React.ReactNode;
children: React.ReactNode;
onClick: () => void;
onClick?: () => void | undefined;
disabled?: boolean;
primary?: boolean;
}) {
const onClickProp = onClick ? { onClick } : {};
return <button
disabled={disabled}
className={clsx("rounded-md text-xs flex items-center gap-1 disabled:text-gray-300 hover:text-gray-600", {
"text-blue-600": primary,
"text-gray-400": !primary,
})}
onClick={onClick}
{...onClickProp}
>
{icon}
{children}

View file

@ -1,9 +1,7 @@
"use client";
import { useState } from "react";
import { WorkflowPrompt } from "@/app/lib/types";
import { Input, Textarea } from "@nextui-org/react";
import { Divider, Input, Textarea } from "@nextui-org/react";
import { z } from "zod";
import MarkdownContent from "@/app/lib/components/markdown-content";
import { ActionButton, Pane } from "./pane";
import { EditableField } from "@/app/lib/components/editable-field";
@ -31,41 +29,46 @@ export function PromptConfig({
]}>
<div className="flex flex-col gap-4">
{prompt.type === "base_prompt" && (
<>
<EditableField
label="Name"
value={prompt.name}
onChange={(value) => {
handleUpdate({
...prompt,
name: value
});
}}
placeholder="Enter prompt name"
validate={(value) => {
if (value.length === 0) {
return { valid: false, errorMessage: "Name cannot be empty" };
}
if (usedPromptNames.has(value)) {
return { valid: false, errorMessage: "This name is already taken" };
}
return { valid: true };
}}
/>
<Divider />
</>
)}
<div className="w-full flex flex-col">
<EditableField
label="Name"
value={prompt.name}
value={prompt.prompt}
onChange={(value) => {
handleUpdate({
...prompt,
name: value
prompt: value
});
}}
placeholder="Enter prompt name"
validate={(value) => {
if (value.length === 0) {
return { valid: false, errorMessage: "Name cannot be empty" };
}
if (usedPromptNames.has(value)) {
return { valid: false, errorMessage: "This name is already taken" };
}
return { valid: true };
}}
placeholder="Edit prompt here..."
markdown
label="Prompt"
multiline
/>
)}
<EditableField
value={prompt.prompt}
onChange={(value) => {
handleUpdate({
...prompt,
prompt: value
});
}}
placeholder="Edit prompt here..."
markdown
label="Prompt"
multiline
/>
</div>
</div>
</Pane>;
}

View file

@ -1,9 +1,111 @@
"use client";
import { WorkflowTool } from "@/app/lib/types";
import { Button, Select, SelectItem, Switch } from "@nextui-org/react";
import { Accordion, AccordionItem, Button, Checkbox, Select, SelectItem, Switch } from "@nextui-org/react";
import { z } from "zod";
import { ActionButton, Pane } from "./pane";
import { EditableField } from "@/app/lib/components/editable-field";
import { Divider } from "@nextui-org/react";
import { Label } from "@/app/lib/components/label";
import { TrashIcon, XIcon } from "lucide-react";
import { useState } from "react";
export function ParameterConfig({
param,
handleUpdate,
handleDelete,
handleRename
}: {
param: {
name: string,
description: string,
type: string,
required: boolean
},
handleUpdate: (name: string, data: {
description: string,
type: string,
required: boolean
}) => void,
handleDelete: (name: string) => void,
handleRename: (oldName: string, newName: string) => void
}) {
return <Pane
title={param.name}
actions={[
<ActionButton
key="delete"
onClick={() => handleDelete(param.name)}
icon={<XIcon size={16} />}
>
Remove
</ActionButton>
]}
>
<div className="flex flex-col gap-2">
<EditableField
label="Name"
value={param.name}
onChange={(newName) => {
if (newName && newName !== param.name) {
handleRename(param.name, newName);
}
}}
/>
<Divider />
<div className="flex flex-col gap-2">
<Label label="Type" />
<Select
variant="bordered"
className="w-52"
size="sm"
selectedKeys={new Set([param.type])}
onSelectionChange={(keys) => {
handleUpdate(param.name, {
...param,
type: Array.from(keys)[0] as string
});
}}
>
{['string', 'number', 'boolean', 'array', 'object'].map(type => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</Select>
</div>
<Divider />
<EditableField
label="Description"
value={param.description}
onChange={(desc) => {
handleUpdate(param.name, {
...param,
description: desc
});
}}
/>
<Divider />
<Checkbox
size="sm"
isSelected={param.required}
onValueChange={() => {
handleUpdate(param.name, {
...param,
required: !param.required
});
}}
>
Required
</Checkbox>
</div>
</Pane>;
}
export function ToolConfig({
tool,
@ -16,6 +118,68 @@ export function ToolConfig({
handleUpdate: (tool: z.infer<typeof WorkflowTool>) => void,
handleClose: () => void
}) {
const [selectedParams, setSelectedParams] = useState(new Set([]));
function handleParamRename(oldName: string, newName: string) {
const newProperties = { ...tool.parameters!.properties };
newProperties[newName] = newProperties[oldName];
delete newProperties[oldName];
const newRequired = [...(tool.parameters?.required || [])];
newRequired.splice(newRequired.indexOf(oldName), 1);
newRequired.push(newName);
handleUpdate({
...tool,
parameters: { ...tool.parameters!, properties: newProperties, required: newRequired }
});
}
function handleParamUpdate(name: string, data: {
description: string,
type: string,
required: boolean
}) {
const newProperties = { ...tool.parameters!.properties };
newProperties[name] = {
type: data.type,
description: data.description
};
const newRequired = [...(tool.parameters?.required || [])];
if (data.required) {
newRequired.push(name);
} else {
newRequired.splice(newRequired.indexOf(name), 1);
}
handleUpdate({
...tool,
parameters: {
...tool.parameters!,
properties: newProperties,
required: newRequired,
}
});
}
function handleParamDelete(paramName: string) {
const newProperties = { ...tool.parameters!.properties };
delete newProperties[paramName];
const newRequired = [...(tool.parameters?.required || [])];
newRequired.splice(newRequired.indexOf(paramName), 1);
handleUpdate({
...tool,
parameters: {
...tool.parameters!,
properties: newProperties,
required: newRequired,
}
});
}
return (
<Pane title={tool.name} actions={[
<ActionButton
@ -47,6 +211,8 @@ export function ToolConfig({
}}
/>
<Divider />
<EditableField
label="Description"
value={tool.description}
@ -57,179 +223,69 @@ export function ToolConfig({
placeholder="Describe what this tool does..."
/>
<div className="flex items-center gap-2">
<Switch
size="sm"
isSelected={tool.mockInPlayground ?? false}
onValueChange={(value) => handleUpdate({
...tool,
mockInPlayground: value
})}
/>
<span>Mock tool in Playground</span>
</div>
<Divider />
<div className="flex flex-col gap-4 w-full">
<div className="text-sm">Parameters:</div>
<Checkbox
size="sm"
isSelected={tool.mockInPlayground ?? false}
onValueChange={(value) => handleUpdate({
...tool,
mockInPlayground: value
})}
>
Mock tool in Playground
</Checkbox>
<Divider />
<Label label="Parameters" />
<div className="ml-4 flex flex-col gap-2">
{Object.entries(tool.parameters?.properties || {}).map(([paramName, param], index) => (
<div key={index} className="border border-gray-300 rounded p-4">
<div className="flex flex-col gap-4">
<EditableField
label="Parameter Name"
value={paramName}
onChange={(newName) => {
if (newName && newName !== paramName) {
const newProperties = { ...tool.parameters!.properties };
newProperties[newName] = newProperties[paramName];
delete newProperties[paramName];
handleUpdate({
...tool,
parameters: {
...tool.parameters!,
properties: newProperties,
required: tool.parameters!.required?.map(
name => name === paramName ? newName : name
) || []
}
});
}
}}
/>
<Select
label="Type"
labelPlacement="outside"
variant="bordered"
selectedKeys={new Set([param.type])}
onSelectionChange={(keys) => {
const newProperties = { ...tool.parameters!.properties };
newProperties[paramName] = {
...newProperties[paramName],
type: Array.from(keys)[0] as string
};
handleUpdate({
...tool,
parameters: {
...tool.parameters!,
properties: newProperties
}
});
}}
>
{['string', 'number', 'boolean', 'array', 'object'].map(type => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</Select>
<EditableField
label="Description"
value={param.description}
onChange={(desc) => {
const newProperties = { ...tool.parameters!.properties };
newProperties[paramName] = {
...newProperties[paramName],
description: desc
};
handleUpdate({
...tool,
parameters: {
...tool.parameters!,
properties: newProperties
}
});
}}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Switch
size="sm"
isSelected={tool.parameters?.required?.includes(paramName)}
onValueChange={() => {
const required = [...(tool.parameters?.required || [])];
const index = required.indexOf(paramName);
if (index === -1) {
required.push(paramName);
} else {
required.splice(index, 1);
}
handleUpdate({
...tool,
parameters: {
...tool.parameters!,
required
}
});
}}
/>
<span>Required</span>
</div>
<Button
variant="bordered"
isIconOnly
onClick={() => {
const newProperties = { ...tool.parameters!.properties };
delete newProperties[paramName];
handleUpdate({
...tool,
parameters: {
...tool.parameters!,
properties: newProperties,
required: tool.parameters!.required?.filter(
name => name !== paramName
) || []
}
});
}}
>
<svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z" />
</svg>
</Button>
</div>
</div>
</div>
))}
<div className="flex justify-end items-center">
<Button
variant="bordered"
startContent={<svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
onClick={() => {
const newParamName = `param${Object.keys(tool.parameters?.properties || {}).length + 1}`;
const newProperties = {
...(tool.parameters?.properties || {}),
[newParamName]: {
type: 'string',
description: ''
}
};
handleUpdate({
...tool,
parameters: {
type: 'object',
properties: newProperties,
required: [...(tool.parameters?.required || []), newParamName]
}
});
<ParameterConfig
key={paramName}
param={{
name: paramName,
description: param.description,
type: param.type,
required: tool.parameters?.required?.includes(paramName) ?? false
}}
>
Add Parameter
</Button>
</div>
handleUpdate={handleParamUpdate}
handleDelete={handleParamDelete}
handleRename={handleParamRename}
/>
))}
</div>
<Button
className="self-start shrink-0"
variant="light"
size="sm"
startContent={<svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
onClick={() => {
const newParamName = `param${Object.keys(tool.parameters?.properties || {}).length + 1}`;
const newProperties = {
...(tool.parameters?.properties || {}),
[newParamName]: {
type: 'string',
description: ''
}
};
handleUpdate({
...tool,
parameters: {
type: 'object',
properties: newProperties,
required: [...(tool.parameters?.required || []), newParamName]
}
});
}}
>
Add Parameter
</Button>
</div>
</Pane>
);