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 { useClickAway } from "@/hooks/use-click-away";
import MarkdownContent from "@/app/lib/components/markdown-content"; import MarkdownContent from "@/app/lib/components/markdown-content";
import clsx from "clsx"; import clsx from "clsx";
import { Label } from "@/app/lib/components/label";
interface EditableFieldProps { interface EditableFieldProps {
value: string; value: string;
@ -86,7 +87,7 @@ export function EditableField({
return ( return (
<div ref={ref} className={clsx("flex flex-col gap-1", className)}> <div ref={ref} className={clsx("flex flex-col gap-1", className)}>
{(label || isEditing && multiline) && <div className="flex items-center gap-2 justify-between"> {(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"> {isEditing && multiline && <div className="flex items-center gap-2">
<Button <Button
size="sm" 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]} remarkPlugins={[remarkGfm]}
components={{ components={{
h1({ children }) { 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 }) { 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 }) { 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 }) { 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 }) { 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 }) { h6({ children }) {
return <h6 className="text-xs font-semibold py-2">{children}</h6> 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(), examples: z.string().optional(),
prompts: z.array(z.string()), prompts: z.array(z.string()),
tools: 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(), 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(), 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(), 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 Link from "next/link";
import { apiV1 } from "rowboat-shared"; import { apiV1 } from "rowboat-shared";
import { EditableField } from "@/app/lib/components/editable-field"; import { EditableField } from "@/app/lib/components/editable-field";
import { MessageSquareIcon, EllipsisIcon } from "lucide-react";
function UserMessage({ content }: { content: string }) { function UserMessage({ content }: { content: string }) {
return <div className="self-end ml-[30%] flex flex-col"> 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 User
</div> </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} /> <MarkdownContent content={content} />
</div> </div>
</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 // show a message icon with a + symbol to expand and show the content
return <div className="self-start mr-[30%]"> 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)}> {!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"> <MessageSquareIcon size={16} />
<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" /> <EllipsisIcon size={16} />
</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>
<span className="hidden group-hover:block text-xs">Show debug message</span> <span className="hidden group-hover:block text-xs">Show debug message</span>
</button>} </button>}
{expanded && <div className="flex flex-col"> {expanded && <div className="flex flex-col">
<div className="flex gap-2 justify-between items-center"> <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'} {sender ?? 'Assistant'}
</div> </div>
<button className="flex items-center gap-1 text-gray-400 hover:text-gray-600" onClick={() => setExpanded(false)}> <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 }) { function AssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
return <div className="self-start mr-[30%] flex flex-col"> return <div className="self-start mr-[30%] flex flex-col">
<div className="flex gap-2 justify-between items-center"> <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'} {sender ?? 'Assistant'}
</div> </div>
<div className="text-gray-400 text-xs pr-3"> <div className="text-gray-400 text-xs pr-3">
{Math.round(latency / 1000)}s {Math.round(latency / 1000)}s
</div> </div>
</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} /> <MarkdownContent content={content} />
</div> </div>
</div>; </div>;
} }
function AssistantMessageLoading() { function AssistantMessageLoading() {
return <div className="self-start mr-[30%] flex flex-col"> return <div className="self-start mr-[30%] flex flex-col text-gray-500 items-start">
<div className="text-gray-500 text-sm ml-3"> <div className="text-gray-500 text-xs ml-3">
Assistant Assistant
</div> </div>
<div className="bg-gray-100 p-3 rounded-lg rounded-bl-none animate-pulse w-20"> <Spinner size="sm" className="mt-2 ml-3" />
<Spinner />
</div>
</div>; </div>;
} }
@ -84,8 +79,8 @@ function UserMessageLoading() {
<div className="text-right text-gray-500 text-sm mr-3"> <div className="text-right text-gray-500 text-sm mr-3">
User User
</div> </div>
<div className="bg-gray-100 p-3 rounded-lg rounded-br-none animate-pulse w-20"> <div className="bg-gray-100 p-3 rounded-lg rounded-br-none animate-pulse w-20 text-gray-800">
<Spinner /> <Spinner size="sm" />
</div> </div>
</div>; </div>;
} }

View file

@ -1,11 +1,13 @@
"use client"; "use client";
import { AgenticAPITool, DataSource, WithStringId, WorkflowAgent, WorkflowPrompt } from "@/app/lib/types"; 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 { z } from "zod";
import { DataSourceIcon } from "@/app/lib/components/datasource-icon"; import { DataSourceIcon } from "@/app/lib/components/datasource-icon";
import { ActionButton, Pane } from "./pane"; import { ActionButton, Pane } from "./pane";
import { EditableField } from "@/app/lib/components/editable-field"; 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({ export function AgentConfig({
agent, agent,
@ -38,7 +40,7 @@ export function AgentConfig({
</ActionButton> </ActionButton>
]}> ]}>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{!agent.locked && ( {!agent.locked && <>
<EditableField <EditableField
key="name" key="name"
label="Name" label="Name"
@ -60,7 +62,8 @@ export function AgentConfig({
return { valid: true }; return { valid: true };
}} }}
/> />
)} <Divider />
</>}
<EditableField <EditableField
key="description" key="description"
@ -75,6 +78,8 @@ export function AgentConfig({
placeholder="Enter a description for this agent" placeholder="Enter a description for this agent"
/> />
<Divider />
<div className="w-full flex flex-col"> <div className="w-full flex flex-col">
<EditableField <EditableField
key="instructions" key="instructions"
@ -90,6 +95,9 @@ export function AgentConfig({
multiline multiline
/> />
</div> </div>
<Divider />
<div className="w-full flex flex-col"> <div className="w-full flex flex-col">
<EditableField <EditableField
key="examples" key="examples"
@ -107,37 +115,29 @@ export function AgentConfig({
/> />
</div> </div>
<div className="flex flex-col gap-2 items-start"> <Divider />
<div className="text-sm">Attach prompts:</div>
<div className="flex gap-4 flex-wrap"> <div className="flex flex-col gap-4 items-start">
{agent.prompts.map((prompt) => ( <Label label="Prompts" />
<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"> <List
<div>{prompt}</div> items={agent.prompts.map((prompt) => ({
<button id: prompt,
onClick={() => { node: <div>{prompt}</div>
const newPrompts = agent.prompts.filter((p) => p !== prompt); }))}
handleUpdate({ onRemove={(id) => {
...agent, const newPrompts = agent.prompts.filter((p) => p !== id);
prompts: newPrompts 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" /> <Dropdown size="sm">
</svg>
</button>
</div>
))}
</div>
<Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
variant="bordered" variant="light"
size="sm" 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"> startContent={<PlusIcon size={16} />}
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
> >
Add prompt Add prompt
</Button> </Button>
@ -154,40 +154,33 @@ export function AgentConfig({
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
</div> </div>
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">RAG:</div> <Divider />
<div className="flex gap-4 flex-wrap">
{agent.ragDataSources?.map((source) => ( <div className="flex flex-col gap-4 items-start">
<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"> <Label label="RAG" />
<div className="flex items-center gap-1"> <List
<DataSourceIcon type={dataSources.find((ds) => ds._id === source)?.data.type} /> items={agent.ragDataSources?.map((source) => ({
<div>{dataSources.find((ds) => ds._id === source)?.name || "Unknown"}</div> id: source,
</div> node: <div className="flex items-center gap-1">
<button <DataSourceIcon type={dataSources.find((ds) => ds._id === source)?.data.type} />
onClick={() => { <div>{dataSources.find((ds) => ds._id === source)?.name || "Unknown"}</div>
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>
</div> </div>
))} })) || []}
</div> onRemove={(id) => {
const newSources = agent.ragDataSources?.filter((s) => s !== id);
handleUpdate({
...agent,
ragDataSources: newSources
});
}}
/>
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
variant="bordered" variant="light"
size="sm" 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"> startContent={<PlusIcon size={16} />}
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
> >
Add data source Add data source
</Button> </Button>
@ -206,72 +199,64 @@ export function AgentConfig({
))} ))}
</DropdownMenu> </DropdownMenu>
</Dropdown> </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>
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">Tools:</div> <Divider />
<div className="flex gap-4 flex-wrap">
{agent.tools.map((tool) => ( {agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && <>
<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"> <Label label="Advanced RAG configuration" />
<div className="font-mono">{tool}</div> <div className="ml-4 flex flex-col gap-4">
<button <Label label="Return type" />
onClick={() => { <RadioGroup
const newTools = agent.tools.filter((t) => t !== tool); size="sm"
handleUpdate({ orientation="horizontal"
...agent, value={agent.ragReturnType}
tools: newTools onValueChange={(value) => handleUpdate({
}); ...agent,
}} ragReturnType: value as z.infer<typeof WorkflowAgent>['ragReturnType']
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"> <Radio value="chunks">Chunks</Radio>
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M6 18 17.94 6M18 18 6.06 6" /> <Radio value="content">Content</Radio>
</svg> </RadioGroup>
</button> <Label label="No. of matches" />
</div> <Input
))} variant="bordered"
size="sm"
className="w-20"
value={agent.ragK.toString()}
onValueChange={(value) => handleUpdate({
...agent,
ragK: parseInt(value)
})}
type="number"
/>
</div> </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> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
variant="bordered" variant="light"
size="sm" 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"> startContent={<PlusIcon size={16} />}
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
> >
Add tool Add tool
</Button> </Button>
@ -288,37 +273,29 @@ export function AgentConfig({
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
</div> </div>
<div className="flex flex-col gap-2 items-start">
<div className="text-sm">Connected agents:</div> <Divider />
<div className="flex gap-4 flex-wrap"> <div className="flex flex-col gap-4 items-start">
{agent.connectedAgents?.map((connectedAgentName) => ( <Label label="Connected agents" />
<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"> <List
<div>{connectedAgentName}</div> items={agent.connectedAgents?.map((connectedAgentName) => ({
<button id: connectedAgentName,
onClick={() => { node: <div>{connectedAgentName}</div>
const newAgents = (agent.connectedAgents || []).filter((a) => a !== connectedAgentName); })) || []}
handleUpdate({ onRemove={(id) => {
...agent, const newAgents = (agent.connectedAgents || []).filter((a) => a !== id);
connectedAgents: newAgents 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>
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
variant="bordered" variant="light"
size="sm" 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"> startContent={<PlusIcon size={16} />}
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>}
> >
Connect agent Connect agent
</Button> </Button>
@ -339,27 +316,30 @@ export function AgentConfig({
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
</div> </div>
<Divider />
<div className="flex flex-col gap-2 items-start"> <div className="flex flex-col gap-2 items-start">
<EditableField <Label label="Model" />
label="Model:" <Select
value={agent.model} variant="bordered"
onChange={(value) => { selectedKeys={[agent.model]}
handleUpdate({ size="sm"
...agent, onSelectionChange={(keys) => handleUpdate({
model: value ...agent,
}); model: keys.currentKey! as z.infer<typeof WorkflowAgent>['model']
}} })}
validate={(value) => {
if (value.length === 0) {
return { valid: false, errorMessage: "Model cannot be empty" };
}
return { valid: true };
}}
className="w-40" className="w-40"
/> >
{WorkflowAgent.shape.model.options.map((model) => (
<SelectItem key={model.value} value={model.value}>{model.value}</SelectItem>
))}
</Select>
</div> </div>
<Divider />
<div className="flex flex-col gap-2 items-start"> <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 <Select
variant="bordered" variant="bordered"
selectedKeys={[agent.controlType]} 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"> return <div className="flex flex-col gap-2 mb-8">
<RawJsonResponse message={message} /> <RawJsonResponse message={message} />
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-2">
{message.content.response.map((part, index) => { {message.content.response.map((part, index) => {
if (part.type === "text") { if (part.type === "text") {
return <div key={index}> return <div key={index} className="text-sm">
<MarkdownContent content={part.content} /> <MarkdownContent content={part.content} />
</div>; </div>;
} else if (part.type === "action") { } else if (part.type === "action") {
@ -158,7 +158,7 @@ function UserMessage({
}: { }: {
message: z.infer<typeof CopilotUserMessage>; 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} /> <MarkdownContent content={message.content} />
</div> </div>
} }
@ -376,7 +376,7 @@ function App({
return <div className="h-full flex flex-col"> return <div className="h-full flex flex-col">
<CopilotContext.Provider value={{ workflow, handleApplyChange, appliedChanges }}> <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) => { {messages.map((m, index) => {
// Calculate if this assistant message is stale // Calculate if this assistant message is stale
const isStale = m.role === 'assistant' && messages.slice(index + 1).some( 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> <div>
{loadingMessage} {loadingMessage}
</div> </div>

View file

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

View file

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

View file

@ -1,9 +1,111 @@
"use client"; "use client";
import { WorkflowTool } from "@/app/lib/types"; 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 { z } from "zod";
import { ActionButton, Pane } from "./pane"; import { ActionButton, Pane } from "./pane";
import { EditableField } from "@/app/lib/components/editable-field"; 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({ export function ToolConfig({
tool, tool,
@ -16,6 +118,68 @@ export function ToolConfig({
handleUpdate: (tool: z.infer<typeof WorkflowTool>) => void, handleUpdate: (tool: z.infer<typeof WorkflowTool>) => void,
handleClose: () => 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 ( return (
<Pane title={tool.name} actions={[ <Pane title={tool.name} actions={[
<ActionButton <ActionButton
@ -47,6 +211,8 @@ export function ToolConfig({
}} }}
/> />
<Divider />
<EditableField <EditableField
label="Description" label="Description"
value={tool.description} value={tool.description}
@ -57,179 +223,69 @@ export function ToolConfig({
placeholder="Describe what this tool does..." placeholder="Describe what this tool does..."
/> />
<div className="flex items-center gap-2"> <Divider />
<Switch
size="sm"
isSelected={tool.mockInPlayground ?? false}
onValueChange={(value) => handleUpdate({
...tool,
mockInPlayground: value
})}
/>
<span>Mock tool in Playground</span>
</div>
<div className="flex flex-col gap-4 w-full"> <Checkbox
<div className="text-sm">Parameters:</div> 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) => ( {Object.entries(tool.parameters?.properties || {}).map(([paramName, param], index) => (
<div key={index} className="border border-gray-300 rounded p-4"> <ParameterConfig
<div className="flex flex-col gap-4"> key={paramName}
<EditableField param={{
label="Parameter Name" name: paramName,
value={paramName} description: param.description,
onChange={(newName) => { type: param.type,
if (newName && newName !== paramName) { required: tool.parameters?.required?.includes(paramName) ?? false
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]
}
});
}} }}
> handleUpdate={handleParamUpdate}
Add Parameter handleDelete={handleParamDelete}
</Button> handleRename={handleParamRename}
</div> />
))}
</div> </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> </div>
</Pane> </Pane>
); );