mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-28 09:56:23 +02:00
ui updates on workflow editor
This commit is contained in:
parent
b6728d270d
commit
988dca96dc
11 changed files with 478 additions and 398 deletions
|
|
@ -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"
|
||||
|
|
|
|||
3
apps/rowboat/app/lib/components/label.tsx
Normal file
3
apps/rowboat/app/lib/components/label.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function Label({ label }: { label: string }) {
|
||||
return <div className="text-xs font-medium text-gray-400 uppercase">{label}</div>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue