mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +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 { 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"
|
||||||
|
|
|
||||||
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]}
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue