ui updates for workflow editor

This commit is contained in:
ramnique 2025-01-18 14:20:40 +05:30
parent a41fe26d4b
commit b6728d270d
14 changed files with 334 additions and 347 deletions

View file

@ -336,7 +336,7 @@ export async function createProject(formData: FormData) {
redirect(`/projects/${projectId}/workflow`);
}
export async function getProjectConfig(projectId: string): Promise<z.infer<typeof Project>> {
export async function getProjectConfig(projectId: string): Promise<WithStringId<z.infer<typeof Project>>> {
await projectAuthCheck(projectId);
const project = await projectsCollection.findOne({
_id: projectId,

View file

@ -84,8 +84,8 @@ export function EditableField({
};
return (
<div ref={ref} className={className}>
<div className="flex items-center gap-2 justify-between">
<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>}
{isEditing && multiline && <div className="flex items-center gap-2">
<Button
@ -111,7 +111,7 @@ export function EditableField({
Save
</Button>
</div>}
</div>
</div>}
{isEditing ? (
multiline ? (
<Textarea

View file

@ -2,7 +2,7 @@
import React from 'react';
import { Textarea, Button } from "@nextui-org/react";
import { CheckIcon, ClipboardIcon } from 'lucide-react';
import { CheckIcon, CopyIcon } from 'lucide-react';
interface EmbedCodeProps {
embedCode: string;
@ -34,7 +34,7 @@ export function EmbedCode({ embedCode }: EmbedCodeProps) {
onClick={handleCopy}
isIconOnly
>
{isCopied ? <CheckIcon size={16} /> : <ClipboardIcon size={16} />}
{isCopied ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
</Button>
</div>
</div>

View file

@ -3,7 +3,7 @@
import { Button, Input } from "@nextui-org/react";
import { useState } from "react";
import { rotateSecret } from "@/app/actions";
import { CheckIcon, ClipboardIcon } from "lucide-react";
import { CheckIcon, CopyIcon } from "lucide-react";
export function Secret({
initialSecret,
@ -69,7 +69,7 @@ export function Secret({
{showCopySuccess ? (
<CheckIcon size={16} />
) : (
<ClipboardIcon size={16} />
<CopyIcon size={16} />
)}
</Button>
) : null

View file

@ -9,7 +9,7 @@ export default async function Layout({
}) {
return <div className="flex h-full">
<Nav projectId={params.projectId} />
<div className="grow p-4 overflow-auto bg-white rounded-tl-lg">
<div className="grow p-2 overflow-auto bg-white rounded-tl-lg">
{children}
</div>
</div >;

View file

@ -4,7 +4,7 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import clsx from "clsx";
import Menu from "./menu";
import { Project } from "@/app/lib/types";
import { Project, WithStringId } from "@/app/lib/types";
import { z } from "zod";
import { getProjectConfig } from "@/app/actions";
import { ChevronsLeftIcon, ChevronsRightIcon } from "lucide-react";
@ -15,7 +15,15 @@ export function Nav({
projectId: string;
}) {
const [collapsed, setCollapsed] = useState(false);
const [project, setProject] = useState<z.infer<typeof Project> | null>(null);
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>>>({
_id: projectId,
name: projectId,
createdAt: "",
lastUpdatedAt: "",
createdByUserId: "",
secret: "",
chatClientId: "",
});
useEffect(() => {
let ignore = false;

View file

@ -72,7 +72,7 @@ export function App({
return <></>;
}
return <Pane title={viewSimulationMenu ? <SimulateLabel /> : "Playground"} actions={[
return <Pane title={viewSimulationMenu ? <SimulateLabel /> : "Chat"} actions={[
<ActionButton
key="new-chat"
icon={<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">

View file

@ -7,8 +7,7 @@ import { AgenticAPIChatRequest, convertToAgenticAPIChatMessages, convertWorkflow
import { ComposeBox } from "./compose-box";
import { Button } from "@nextui-org/react";
import { apiV1 } from "rowboat-shared";
import { CheckIcon, ClipboardIcon } from "lucide-react";
import { CopyIcon } from "lucide-react";
import { CheckIcon, CopyIcon } from "lucide-react";
export function Chat({
chat,
@ -278,7 +277,7 @@ export function Chat({
{showCopySuccess ? (
<CheckIcon size={16} />
) : (
<ClipboardIcon size={16} />
<CopyIcon size={16} />
)}
</Button>
<Messages

View file

@ -1,93 +0,0 @@
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-org/react";
import { WorkflowAgent } from "@/app/lib/types";
import { z } from "zod";
import { useRef, useEffect } from "react";
import { ActionButton, Pane } from "./pane";
export function AgentsList({
agents,
handleSelectAgent,
handleAddAgent,
handleToggleAgent,
selectedAgent,
handleSetMainAgent,
handleDeleteAgent,
startAgentName,
}: {
agents: z.infer<typeof WorkflowAgent>[];
handleSelectAgent: (name: string) => void;
handleAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
handleToggleAgent: (name: string) => void;
selectedAgent: string | null;
handleSetMainAgent: (name: string) => void;
handleDeleteAgent: (name: string) => void;
startAgentName: string | null;
}) {
const selectedAgentRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
const selectedAgentIndex = agents.findIndex(agent => agent.name === selectedAgent);
if (selectedAgentIndex !== -1 && selectedAgentRef.current) {
selectedAgentRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [selectedAgent, agents]);
return <Pane title="Agents" actions={[
<ActionButton
key="add"
icon={<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="2" d="M5 12h14m-7 7V5" />
</svg>}
onClick={() => handleAddAgent({})}
>
Add
</ActionButton>
]}>
<div className="overflow-auto flex flex-col justify-start">
{agents.map((agent, index) => (
<button
key={index}
ref={selectedAgent === agent.name ? selectedAgentRef : null}
onClick={() => handleSelectAgent(agent.name)}
className={`flex items-center justify-between rounded-md px-3 py-2 ${selectedAgent === agent.name ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
>
<div className={`truncate ${agent.disabled ? 'text-gray-400' : ''}`}>{agent.name}</div>
<div className="flex items-center gap-2">
{startAgentName === agent.name && <div className="text-xs border bg-blue-500 text-white px-2 py-1 rounded-md">Start</div>}
<Dropdown key={agent.name}>
<DropdownTrigger>
<svg className="w-6 h-6 text-gray-400" 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="3" d="M12 6h.01M12 12h.01M12 18h.01" />
</svg>
</DropdownTrigger>
<DropdownMenu
disabledKeys={[
...(!agent.toggleAble ? ['toggle'] : []),
...(agent.locked ? ['delete', 'set-main-agent'] : []),
...(startAgentName === agent.name ? ['set-main-agent', 'delete', 'toggle'] : []),
]}
onAction={(key) => {
switch (key) {
case 'set-main-agent':
handleSetMainAgent(agent.name);
break;
case 'delete':
handleDeleteAgent(agent.name);
break;
case 'toggle':
handleToggleAgent(agent.name);
break;
}
}}
>
<DropdownItem key="set-main-agent">Set as start agent</DropdownItem>
<DropdownItem key="toggle">{agent.disabled ? 'Enable' : 'Disable'}</DropdownItem>
<DropdownItem key="delete" className="text-danger">Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</button>
))}
</div>
</Pane>;
}

View file

@ -0,0 +1,237 @@
import { z } from "zod";
import { WorkflowAgent, WorkflowPrompt, AgenticAPITool } from "@/app/lib/types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-org/react";
import { useRef, useEffect } from "react";
import { ActionButton, Pane } from "./pane";
import clsx from "clsx";
interface EntityListProps {
agents: z.infer<typeof WorkflowAgent>[];
tools: z.infer<typeof AgenticAPITool>[];
prompts: z.infer<typeof WorkflowPrompt>[];
selectedEntity: {
type: "agent" | "tool" | "prompt";
name: string;
} | null;
startAgentName: string | null;
onSelectAgent: (name: string) => void;
onSelectTool: (name: string) => void;
onSelectPrompt: (name: string) => void;
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
onAddTool: (tool: Partial<z.infer<typeof AgenticAPITool>>) => void;
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
onToggleAgent: (name: string) => void;
onSetMainAgent: (name: string) => void;
onDeleteAgent: (name: string) => void;
onDeleteTool: (name: string) => void;
onDeletePrompt: (name: string) => void;
}
function SectionHeader({ title, onAdd }: { title: string; onAdd: () => void }) {
return (
<div className="flex items-center justify-between px-2 py-1 mt-4 first:mt-0 border-b border-gray-200">
<div className="text-xs font-semibold text-gray-400 uppercase">{title}</div>
<ActionButton
icon={<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="2" d="M5 12h14m-7 7V5" />
</svg>}
onClick={onAdd}
>
Add
</ActionButton>
</div>
);
}
function ListItem({
name,
isSelected,
onClick,
disabled,
rightElement,
selectedRef
}: {
name: string;
isSelected: boolean;
onClick: () => void;
disabled?: boolean;
rightElement?: React.ReactNode;
selectedRef?: React.RefObject<HTMLButtonElement>;
}) {
return (
<button
ref={selectedRef as any}
onClick={onClick}
className={clsx("flex items-center justify-between rounded-md px-2 py-1", {
"bg-gray-100": isSelected,
"hover:bg-gray-50": !isSelected,
})}
>
<div className={clsx("truncate text-sm", {
"text-gray-400": disabled,
})}>{name}</div>
{rightElement}
</button>
);
}
export function EntityList({
agents,
tools,
prompts,
selectedEntity,
startAgentName,
onSelectAgent,
onSelectTool,
onSelectPrompt,
onAddAgent,
onAddTool,
onAddPrompt,
onToggleAgent,
onSetMainAgent,
onDeleteAgent,
onDeleteTool,
onDeletePrompt,
}: EntityListProps) {
const selectedRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
if (selectedEntity && selectedRef.current) {
selectedRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [selectedEntity]);
return (
<Pane title="Index">
<div className="overflow-auto flex flex-col gap-1 justify-start">
{/* Agents Section */}
<SectionHeader title="Agents" onAdd={() => onAddAgent({})} />
{agents.map((agent, index) => (
<ListItem
key={`agent-${index}`}
name={agent.name}
isSelected={selectedEntity?.type === "agent" && selectedEntity.name === agent.name}
onClick={() => onSelectAgent(agent.name)}
disabled={agent.disabled}
selectedRef={selectedEntity?.type === "agent" && selectedEntity.name === agent.name ? selectedRef : undefined}
rightElement={
<div className="flex items-center gap-2">
{startAgentName === agent.name && (
<div className="text-xs border bg-blue-500 text-white px-2 py-1 rounded-md">Start</div>
)}
<AgentDropdown
agent={agent}
isStartAgent={startAgentName === agent.name}
onToggle={onToggleAgent}
onSetMainAgent={onSetMainAgent}
onDelete={onDeleteAgent}
/>
</div>
}
/>
))}
{/* Tools Section */}
<SectionHeader title="Tools" onAdd={() => onAddTool({})} />
{tools.map((tool, index) => (
<ListItem
key={`tool-${index}`}
name={tool.name}
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
onClick={() => onSelectTool(tool.name)}
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
rightElement={<EntityDropdown name={tool.name} onDelete={onDeleteTool} />}
/>
))}
{/* Prompts Section */}
<SectionHeader title="Prompts" onAdd={() => onAddPrompt({})} />
{prompts.map((prompt, index) => (
<ListItem
key={`prompt-${index}`}
name={prompt.name}
isSelected={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name}
onClick={() => onSelectPrompt(prompt.name)}
selectedRef={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name ? selectedRef : undefined}
rightElement={<EntityDropdown name={prompt.name} onDelete={onDeletePrompt} />}
/>
))}
</div>
</Pane>
);
}
function AgentDropdown({
agent,
isStartAgent,
onToggle,
onSetMainAgent,
onDelete
}: {
agent: z.infer<typeof WorkflowAgent>;
isStartAgent: boolean;
onToggle: (name: string) => void;
onSetMainAgent: (name: string) => void;
onDelete: (name: string) => void;
}) {
return (
<Dropdown>
<DropdownTrigger>
<svg className="w-4 h-4 text-gray-400" 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="3" d="M12 6h.01M12 12h.01M12 18h.01" />
</svg>
</DropdownTrigger>
<DropdownMenu
disabledKeys={[
...(!agent.toggleAble ? ['toggle'] : []),
...(agent.locked ? ['delete', 'set-main-agent'] : []),
...(isStartAgent ? ['set-main-agent', 'delete', 'toggle'] : []),
]}
onAction={(key) => {
switch (key) {
case 'set-main-agent':
onSetMainAgent(agent.name);
break;
case 'delete':
onDelete(agent.name);
break;
case 'toggle':
onToggle(agent.name);
break;
}
}}
>
<DropdownItem key="set-main-agent">Set as start agent</DropdownItem>
<DropdownItem key="toggle">{agent.disabled ? 'Enable' : 'Disable'}</DropdownItem>
<DropdownItem key="delete" className="text-danger">Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
);
}
function EntityDropdown({
name,
onDelete
}: {
name: string;
onDelete: (name: string) => void;
}) {
return (
<Dropdown>
<DropdownTrigger>
<svg className="w-4 h-4 text-gray-400" 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="3" d="M12 6h.01M12 12h.01M12 18h.01" />
</svg>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
if (key === 'delete') {
onDelete(name);
}
}}
>
<DropdownItem key="delete" className="text-danger">Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
);
}

View file

@ -1,24 +1,36 @@
import clsx from "clsx";
export function Pane({
title,
actions,
actions = null,
children,
fancy = false,
}: {
title: React.ReactNode;
actions: React.ReactNode[];
actions?: React.ReactNode[] | null;
children: React.ReactNode;
fancy?: boolean;
}) {
return <div className={`h-full flex flex-col overflow-auto border rounded-md ${fancy ? 'border-blue-200' : 'border-gray-200'}`}>
<div className={`shrink-0 flex justify-between items-center gap-2 px-2 py-1 bg-gray-50 rounded-t-md ${fancy ? 'bg-blue-50' : ''}`}>
<div className={`text-sm ${fancy ? 'text-blue-600' : 'text-gray-600'} uppercase font-semibold`}>
return <div className={clsx("h-full flex flex-col overflow-auto rounded-md p-1", {
"bg-gray-100": !fancy,
"bg-blue-100": fancy,
})}>
<div className="shrink-0 flex justify-between items-center gap-2 px-2 py-1 rounded-t-sm">
<div className={clsx("text-xs font-semibold uppercase", {
"text-gray-400": !fancy,
"text-blue-500": fancy,
})}>
{title}
</div>
<div className="rounded-md hover:text-gray-800 px-2 py-1 text-gray-600 text-sm flex items-center gap-1">
{!actions && <div className="w-4 h-4" />}
{actions && <div className={clsx("rounded-md hover:text-gray-800 px-2 text-sm flex items-center gap-1", {
"text-blue-600": fancy,
"text-gray-400": !fancy,
})}>
{actions}
</div>
</div>}
</div>
<div className="grow overflow-auto flex flex-col justify-start p-2">
<div className="grow bg-white rounded-md overflow-auto flex flex-col justify-start p-2">
{children}
</div>
</div>;
@ -39,7 +51,10 @@ export function ActionButton({
}) {
return <button
disabled={disabled}
className={`rounded-md hover:text-gray-800 px-2 py-1 ${primary ? 'text-blue-600' : 'text-gray-600'} text-sm flex items-center gap-1 disabled:text-gray-300`}
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}
>
{icon}

View file

@ -1,74 +0,0 @@
import { z } from "zod";
import { WorkflowPrompt } from "@/app/lib/types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-org/react";
import { useRef, useEffect } from "react";
import { ActionButton, Pane } from "./pane";
export function PromptsList({
prompts,
handleSelectPrompt,
handleAddPrompt,
selectedPrompt,
handleDeletePrompt,
}: {
prompts: z.infer<typeof WorkflowPrompt>[];
handleSelectPrompt: (name: string) => void;
handleAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
selectedPrompt: string | null;
handleDeletePrompt: (name: string) => void;
}) {
const selectedPromptRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
const selectedPromptIndex = prompts.findIndex(prompt => prompt.name === selectedPrompt);
if (selectedPromptIndex !== -1 && selectedPromptRef.current) {
selectedPromptRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [selectedPrompt, prompts]);
return <Pane title="Prompts" actions={[
<ActionButton
key="add"
icon={<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="2" d="M5 12h14m-7 7V5" />
</svg>}
onClick={() => handleAddPrompt({})}
>
Add
</ActionButton>
]}>
<div className="overflow-auto flex flex-col justify-start">
{prompts.map((prompt, index) => (
<button
key={index}
ref={selectedPrompt === prompt.name ? selectedPromptRef : null}
onClick={() => handleSelectPrompt(prompt.name)}
className={`flex items-center justify-between rounded-md px-3 py-2 ${selectedPrompt === prompt.name ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
>
<div className="flex items-center gap-2">
{prompt.type === 'style_prompt' && <svg className="w-5 h-5 text-gray-500" 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="1" d="M20 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6h-2m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4" />
</svg>}
<div className="truncate">{prompt.name}</div>
</div>
<Dropdown key={prompt.name}>
<DropdownTrigger>
<svg className="w-6 h-6 text-gray-400" 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="3" d="M12 6h.01M12 12h.01M12 18h.01" />
</svg>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
if (key === 'delete') {
handleDeletePrompt(prompt.name);
}
}}
>
<DropdownItem key="delete" className="text-danger">Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
</button>
))}
</div>
</Pane>;
}

View file

@ -1,71 +0,0 @@
import { z } from "zod";
import { AgenticAPITool } from "@/app/lib/types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-org/react";
import { useRef, useEffect } from "react";
import { ActionButton, Pane } from "./pane";
export function ToolsList({
tools,
handleSelectTool,
handleAddTool,
selectedTool,
handleDeleteTool,
}: {
tools: z.infer<typeof AgenticAPITool>[];
handleSelectTool: (name: string) => void;
handleAddTool: (tool: Partial<z.infer<typeof AgenticAPITool>>) => void;
selectedTool: string | null;
handleDeleteTool: (name: string) => void;
}) {
const selectedToolRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
const selectedToolIndex = tools.findIndex(tool => tool.name === selectedTool);
if (selectedToolIndex !== -1 && selectedToolRef.current) {
selectedToolRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [selectedTool, tools]);
return <Pane title="Tools" actions={[
<ActionButton
key="add"
icon={<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="2" d="M5 12h14m-7 7V5" />
</svg>}
onClick={() => handleAddTool({})}
>
Add
</ActionButton>
]}>
<div className="overflow-auto flex flex-col justify-start">
{tools.map((tool, index) => (
<button
key={index}
ref={selectedTool === tool.name ? selectedToolRef : null}
onClick={() => handleSelectTool(tool.name)}
className={`flex items-center justify-between rounded-md px-3 py-2 ${selectedTool === tool.name ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
>
<div className="flex items-center gap-2">
<div>{tool.name}</div>
</div>
<Dropdown key={tool.name}>
<DropdownTrigger>
<svg className="w-6 h-6 text-gray-400" 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="3" d="M12 6h.01M12 12h.01M12 18h.01" />
</svg>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
if (key === 'delete') {
handleDeleteTool(tool.name);
}
}}
>
<DropdownItem key="delete" className="text-danger">Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
</button>
))}
</div>
</Pane>;
}

View file

@ -6,11 +6,8 @@ import { AgentConfig } from "./agent_config";
import { ToolConfig } from "./tool_config";
import { App as ChatApp } from "../playground/app";
import { z } from "zod";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger, Spinner } from "@nextui-org/react";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip } from "@nextui-org/react";
import { PromptConfig } from "./prompt_config";
import { AgentsList } from "./agents_list";
import { PromptsList } from "./prompts_list";
import { ToolsList } from "./tools_list";
import { EditableField } from "@/app/lib/components/editable-field";
import { RelativeTime } from "@primer/react";
@ -24,7 +21,8 @@ import { apiV1 } from "rowboat-shared";
import { publishWorkflow, renameWorkflow, saveWorkflow } from "@/app/actions";
import { PublishedBadge } from "./published_badge";
import { BackIcon, HamburgerIcon, WorkflowIcon } from "@/app/lib/components/icons";
import { ClipboardIcon, Layers2Icon, RadioIcon } from "lucide-react";
import { CopyIcon, Layers2Icon, RadioIcon, RedoIcon, UndoIcon } from "lucide-react";
import { EntityList } from "./entity_list";
enablePatches();
@ -640,30 +638,22 @@ export function WorkflowEditor({
return <div className="flex flex-col h-full relative">
<div className="shrink-0 flex justify-between items-center pb-2">
<div className="flex items-center gap-2">
<div className="font-semibold">Workflow</div>
<div className="flex items-center gap-1">
<WorkflowIcon />
<div className="font-semibold">
<EditableField
key={state.present.workflow._id}
value={state.present.workflow?.name || ''}
onChange={handleRenameWorkflow}
placeholder="Name this version"
/>
</div>
{state.present.publishing && <Spinner size="sm" />}
{isLive && <PublishedBadge />}
</div>
<div className="flex items-center gap-1 border-1 border-gray-200 rounded-md px-2 text-gray-800">
<WorkflowIcon size={16} />
<EditableField
key={state.present.workflow._id}
value={state.present.workflow?.name || ''}
onChange={handleRenameWorkflow}
placeholder="Name this version"
className="text-sm font-semibold"
/>
{state.present.publishing && <Spinner size="sm" />}
{isLive && <PublishedBadge />}
<Dropdown>
<DropdownTrigger>
<Button
isIconOnly
variant="bordered"
size="sm"
>
<button className="p-1 text-gray-400 hover:text-black">
<HamburgerIcon size={16} />
</Button>
</button>
</DropdownTrigger>
<DropdownMenu
disabledKeys={[
@ -706,7 +696,7 @@ export function WorkflowEditor({
</DropdownItem>
<DropdownItem
key="clipboard"
startContent={<ClipboardIcon size={16} />}
startContent={<CopyIcon size={16} />}
>
Copy as JSON
</DropdownItem>
@ -729,82 +719,58 @@ export function WorkflowEditor({
Clone this version
</Button>
</div>}
{!isLive && <>
{state.present.saving && <div className="flex items-center gap-2">
{!isLive && <div className="text-xs text-gray-400">
{state.present.saving && <div className="flex items-center gap-1">
<Spinner size="sm" />
<div className="text-sm text-gray-500">Saving...</div>
<div>Saving...</div>
</div>}
{!state.present.saving && state.present.workflow && <div className="text-sm text-gray-500">
{!state.present.saving && state.present.workflow && <div>
Updated <RelativeTime date={new Date(state.present.workflow.lastUpdatedAt)} />
</div>}
</>}
</div>}
{!isLive && <>
<Button
isIconOnly
variant="bordered"
<button
className="p-1 text-gray-400 hover:text-black"
title="Undo"
size="sm"
disabled={state.currentIndex <= 0}
onClick={() => dispatch({ type: "undo" })}
>
<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="M3 9h13a5 5 0 0 1 0 10H7M3 9l4-4M3 9l4 4" />
</svg>
</Button>
<Button
isIconOnly
variant="bordered"
<UndoIcon size={16} />
</button>
<button
className="p-1 text-gray-400 hover:text-black"
title="Redo"
size="sm"
disabled={state.currentIndex >= state.patches.length}
onClick={() => dispatch({ type: "redo" })}
>
<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="M21 9H8a5 5 0 0 0 0 10h9m4-10-4-4m4 4-4 4" />
</svg>
</Button>
<RedoIcon size={16} />
</button>
</>}
</div>
</div>
<ResizablePanelGroup direction="horizontal" className="grow flex overflow-auto gap-1">
<ResizablePanel minSize={10} defaultSize={20}>
<ResizablePanelGroup direction="vertical" className="flex flex-col gap-1">
<ResizablePanel minSize={10} defaultSize={50}>
<AgentsList
agents={state.present.workflow.agents}
handleSelectAgent={handleSelectAgent}
handleAddAgent={handleAddAgent}
selectedAgent={state.present.selection?.type === "agent" ? state.present.selection.name : null}
handleToggleAgent={handleToggleAgent}
handleSetMainAgent={handleSetMainAgent}
handleDeleteAgent={handleDeleteAgent}
startAgentName={state.present.workflow.startAgent}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel minSize={10} defaultSize={30}>
<ToolsList
tools={state.present.workflow.tools}
handleSelectTool={handleSelectTool}
handleAddTool={handleAddTool}
selectedTool={state.present.selection?.type === "tool" ? state.present.selection.name : null}
handleDeleteTool={handleDeleteTool}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel minSize={10} defaultSize={20}>
<PromptsList
prompts={state.present.workflow.prompts}
handleSelectPrompt={handleSelectPrompt}
handleAddPrompt={handleAddPrompt}
selectedPrompt={state.present.selection?.type === "prompt" ? state.present.selection.name : null}
handleDeletePrompt={handleDeletePrompt}
/>
</ResizablePanel>
</ResizablePanelGroup>
<ResizablePanel minSize={10} defaultSize={15}>
<EntityList
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
selectedEntity={state.present.selection}
startAgentName={state.present.workflow.startAgent}
onSelectAgent={handleSelectAgent}
onSelectTool={handleSelectTool}
onSelectPrompt={handleSelectPrompt}
onAddAgent={handleAddAgent}
onAddTool={handleAddTool}
onAddPrompt={handleAddPrompt}
onToggleAgent={handleToggleAgent}
onSetMainAgent={handleSetMainAgent}
onDeleteAgent={handleDeleteAgent}
onDeleteTool={handleDeleteTool}
onDeletePrompt={handleDeletePrompt}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel minSize={20} defaultSize={50} className="overflow-auto">
<ResizablePanel minSize={20} defaultSize={60} className="overflow-auto">
<ChatApp
key={'' + state.present.chatKey}
hidden={state.present.selection !== null}
@ -839,7 +805,7 @@ export function WorkflowEditor({
/>}
</ResizablePanel>
<ResizableHandle />
<ResizablePanel minSize={10} defaultSize={30}>
<ResizablePanel minSize={10} defaultSize={25}>
<Copilot
projectId={state.present.workflow.projectId}
workflow={state.present.workflow}