mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-05 13:22:38 +02:00
ui updates for workflow editor
This commit is contained in:
parent
a41fe26d4b
commit
b6728d270d
14 changed files with 334 additions and 347 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 >;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
237
apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx
Normal file
237
apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue