mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
Merge pull request #28 from rowboatlabs/ui_changes
UI Changes: Copilot Collapse, Tooltips, Headings
This commit is contained in:
commit
1edf2e0e18
5 changed files with 185 additions and 73 deletions
|
|
@ -98,22 +98,26 @@ export function App({
|
|||
}
|
||||
|
||||
return (
|
||||
<Pane title="Chat" actions={[
|
||||
<ActionButton
|
||||
key="new-chat"
|
||||
icon={<MessageSquarePlusIcon size={16} />}
|
||||
onClick={handleNewChatButtonClick}
|
||||
>
|
||||
New chat
|
||||
</ActionButton>,
|
||||
<ActionButton
|
||||
key="simulate"
|
||||
icon={<PlayIcon size={16} />}
|
||||
onClick={handleSimulateButtonClick}
|
||||
>
|
||||
Simulate
|
||||
</ActionButton>,
|
||||
]}>
|
||||
<Pane
|
||||
title="PLAYGROUND"
|
||||
tooltip="Test your agents and see their responses in this interactive chat interface"
|
||||
actions={[
|
||||
<ActionButton
|
||||
key="new-chat"
|
||||
icon={<MessageSquarePlusIcon size={16} />}
|
||||
onClick={handleNewChatButtonClick}
|
||||
>
|
||||
New chat
|
||||
</ActionButton>,
|
||||
<ActionButton
|
||||
key="simulate"
|
||||
icon={<PlayIcon size={16} />}
|
||||
onClick={handleSimulateButtonClick}
|
||||
>
|
||||
Simulate
|
||||
</ActionButton>,
|
||||
]}
|
||||
>
|
||||
<div className="h-full overflow-auto">
|
||||
{loadingChat && <div className="flex justify-center items-center h-full">
|
||||
<Spinner />
|
||||
|
|
|
|||
|
|
@ -174,23 +174,35 @@ function App({
|
|||
workflow,
|
||||
dispatch,
|
||||
chatContext=undefined,
|
||||
messages,
|
||||
setMessages,
|
||||
loadingResponse,
|
||||
setLoadingResponse,
|
||||
loadingMessage,
|
||||
setLoadingMessage,
|
||||
responseError,
|
||||
setResponseError,
|
||||
}: {
|
||||
projectId: string;
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
dispatch: (action: WorkflowDispatch) => void;
|
||||
chatContext?: z.infer<typeof CopilotChatContext>;
|
||||
messages: z.infer<typeof CopilotMessage>[];
|
||||
setMessages: (messages: z.infer<typeof CopilotMessage>[]) => void;
|
||||
loadingResponse: boolean;
|
||||
setLoadingResponse: (loading: boolean) => void;
|
||||
loadingMessage: string;
|
||||
setLoadingMessage: (message: string) => void;
|
||||
responseError: string | null;
|
||||
setResponseError: (error: string | null) => void;
|
||||
}) {
|
||||
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
||||
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||
const [loadingMessage, setLoadingMessage] = useState("Thinking...");
|
||||
const [responseError, setResponseError] = useState<string | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
|
||||
const [discardContext, setDiscardContext] = useState(false);
|
||||
const [lastRequest, setLastRequest] = useState<unknown | null>(null);
|
||||
const [lastResponse, setLastResponse] = useState<unknown | null>(null);
|
||||
|
||||
// Cycle through loading messages until reaching the last one
|
||||
// First useEffect for loading messages
|
||||
useEffect(() => {
|
||||
setLoadingMessage("Thinking");
|
||||
if (!loadingResponse) return;
|
||||
|
|
@ -210,7 +222,7 @@ function App({
|
|||
}, 4000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [loadingResponse, messages]);
|
||||
}, [loadingResponse, setLoadingMessage]);
|
||||
|
||||
// Reset discardContext when chatContext changes
|
||||
useEffect(() => {
|
||||
|
|
@ -328,7 +340,7 @@ function App({
|
|||
}
|
||||
}, [dispatch, appliedChanges, messages]);
|
||||
|
||||
// get copilot response
|
||||
// Second useEffect for copilot response
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
|
|
@ -383,7 +395,16 @@ function App({
|
|||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [messages, projectId, responseError, workflow, effectiveContext]);
|
||||
}, [
|
||||
messages,
|
||||
projectId,
|
||||
responseError,
|
||||
workflow,
|
||||
effectiveContext,
|
||||
setLoadingResponse,
|
||||
setMessages,
|
||||
setResponseError
|
||||
]);
|
||||
|
||||
function handleCopyChat() {
|
||||
const jsonString = JSON.stringify({
|
||||
|
|
@ -483,40 +504,63 @@ export function Copilot({
|
|||
workflow,
|
||||
chatContext=undefined,
|
||||
dispatch,
|
||||
onNewChat,
|
||||
messages,
|
||||
setMessages,
|
||||
loadingResponse,
|
||||
setLoadingResponse,
|
||||
loadingMessage,
|
||||
setLoadingMessage,
|
||||
responseError,
|
||||
setResponseError,
|
||||
}: {
|
||||
projectId: string;
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
chatContext?: z.infer<typeof CopilotChatContext>;
|
||||
dispatch: (action: WorkflowDispatch) => void;
|
||||
onNewChat: () => void;
|
||||
messages: z.infer<typeof CopilotMessage>[];
|
||||
setMessages: (messages: z.infer<typeof CopilotMessage>[]) => void;
|
||||
loadingResponse: boolean;
|
||||
setLoadingResponse: (loading: boolean) => void;
|
||||
loadingMessage: string;
|
||||
setLoadingMessage: (message: string) => void;
|
||||
responseError: string | null;
|
||||
setResponseError: (error: string | null) => void;
|
||||
}) {
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
function handleNewChat() {
|
||||
setKey(key + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pane fancy title="Copilot" actions={[
|
||||
<ActionButton
|
||||
key="ask"
|
||||
primary
|
||||
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={handleNewChat}
|
||||
>
|
||||
Ask
|
||||
</ActionButton>
|
||||
]}>
|
||||
<Pane
|
||||
fancy
|
||||
title="COPILOT"
|
||||
tooltip="Get AI assistance for creating and improving your multi-agent system"
|
||||
actions={[
|
||||
<ActionButton
|
||||
key="ask"
|
||||
primary
|
||||
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={onNewChat}
|
||||
>
|
||||
New
|
||||
</ActionButton>
|
||||
]}
|
||||
>
|
||||
<App
|
||||
key={key}
|
||||
projectId={projectId}
|
||||
workflow={workflow}
|
||||
dispatch={dispatch}
|
||||
chatContext={chatContext}
|
||||
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
loadingResponse={loadingResponse}
|
||||
setLoadingResponse={setLoadingResponse}
|
||||
loadingMessage={loadingMessage}
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
responseError={responseError}
|
||||
setResponseError={setResponseError}
|
||||
/>
|
||||
</Pane>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -105,7 +105,10 @@ export function EntityList({
|
|||
}, [selectedEntity]);
|
||||
|
||||
return (
|
||||
<Pane title="Index">
|
||||
<Pane
|
||||
title="WORKFLOW"
|
||||
tooltip="Browse and manage your agents, tools, and prompts in this sidebar"
|
||||
>
|
||||
<div className="overflow-auto flex flex-col gap-1 justify-start">
|
||||
{/* Agents Section */}
|
||||
<SectionHeader title="Agents" onAdd={() => onAddAgent({})} />
|
||||
|
|
|
|||
|
|
@ -1,26 +1,44 @@
|
|||
import clsx from "clsx";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
|
||||
export function Pane({
|
||||
title,
|
||||
actions = null,
|
||||
children,
|
||||
fancy = false,
|
||||
tooltip = null,
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
actions?: React.ReactNode[] | null;
|
||||
children: React.ReactNode;
|
||||
fancy?: boolean;
|
||||
tooltip?: string | null;
|
||||
}) {
|
||||
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 className="flex items-center gap-1">
|
||||
<div className={clsx("text-xs font-semibold uppercase", {
|
||||
"text-gray-400": !fancy,
|
||||
"text-blue-500": fancy,
|
||||
})}>
|
||||
{title}
|
||||
</div>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
content={tooltip}
|
||||
placement="right"
|
||||
className="cursor-help"
|
||||
>
|
||||
<InfoIcon size={12} className={clsx({
|
||||
"text-gray-400": !fancy,
|
||||
"text-blue-500": fancy,
|
||||
})} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{!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-2", {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ import { apiV1 } from "rowboat-shared";
|
|||
import { publishWorkflow, renameWorkflow, saveWorkflow } from "../../../actions/workflow_actions";
|
||||
import { PublishedBadge } from "./published_badge";
|
||||
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
||||
import { CopyIcon, Layers2Icon, RadioIcon, RedoIcon, UndoIcon } from "lucide-react";
|
||||
import { CopyIcon, Layers2Icon, RadioIcon, RedoIcon, Sparkles, UndoIcon } from "lucide-react";
|
||||
import { EntityList } from "./entity_list";
|
||||
import { CopilotMessage } from "../../../lib/types/copilot_types";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
enablePatches();
|
||||
|
||||
|
|
@ -524,6 +527,13 @@ export function WorkflowEditor({
|
|||
const saving = useRef(false);
|
||||
const isLive = state.present.workflow._id == state.present.publishedWorkflowId;
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||||
const [showCopilot, setShowCopilot] = useState(false);
|
||||
const [copilotWidth, setCopilotWidth] = useState(25);
|
||||
const [copilotKey, setCopilotKey] = useState(0);
|
||||
const [copilotMessages, setCopilotMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
||||
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||
const [loadingMessage, setLoadingMessage] = useState("Thinking...");
|
||||
const [responseError, setResponseError] = useState<string | null>(null);
|
||||
|
||||
console.log(`workflow editor chat key: ${state.present.chatKey}`);
|
||||
|
||||
|
|
@ -752,7 +762,7 @@ export function WorkflowEditor({
|
|||
</div>}
|
||||
{!isLive && <>
|
||||
<button
|
||||
className="p-1 text-gray-400 hover:text-black"
|
||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||
title="Undo"
|
||||
disabled={state.currentIndex <= 0}
|
||||
onClick={() => dispatch({ type: "undo" })}
|
||||
|
|
@ -760,13 +770,20 @@ export function WorkflowEditor({
|
|||
<UndoIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 text-gray-400 hover:text-black"
|
||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||
title="Redo"
|
||||
disabled={state.currentIndex >= state.patches.length}
|
||||
onClick={() => dispatch({ type: "redo" })}
|
||||
>
|
||||
<RedoIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 text-blue-600 hover:text-blue-800 hover:cursor-pointer"
|
||||
title="Toggle Copilot"
|
||||
onClick={() => setShowCopilot(!showCopilot)}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
</button>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -792,7 +809,11 @@ export function WorkflowEditor({
|
|||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel minSize={20} defaultSize={60} className="overflow-auto">
|
||||
<ResizablePanel
|
||||
minSize={20}
|
||||
defaultSize={showCopilot ? 85 - copilotWidth : 85}
|
||||
className="overflow-auto"
|
||||
>
|
||||
<ChatApp
|
||||
key={'' + state.present.chatKey}
|
||||
hidden={state.present.selection !== null}
|
||||
|
|
@ -826,23 +847,45 @@ export function WorkflowEditor({
|
|||
handleClose={handleUnselectPrompt}
|
||||
/>}
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel minSize={10} defaultSize={25}>
|
||||
<Copilot
|
||||
projectId={state.present.workflow.projectId}
|
||||
workflow={state.present.workflow}
|
||||
dispatch={dispatch}
|
||||
chatContext={
|
||||
state.present.selection ? {
|
||||
type: state.present.selection.type,
|
||||
name: state.present.selection.name
|
||||
} : chatMessages.length > 0 ? {
|
||||
type: 'chat',
|
||||
messages: chatMessages
|
||||
} : undefined
|
||||
}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
{showCopilot && <>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
minSize={10}
|
||||
defaultSize={copilotWidth}
|
||||
onResize={(size) => setCopilotWidth(size)}
|
||||
>
|
||||
<Copilot
|
||||
key={copilotKey}
|
||||
projectId={state.present.workflow.projectId}
|
||||
workflow={state.present.workflow}
|
||||
dispatch={dispatch}
|
||||
chatContext={
|
||||
state.present.selection ? {
|
||||
type: state.present.selection.type,
|
||||
name: state.present.selection.name
|
||||
} : chatMessages.length > 0 ? {
|
||||
type: 'chat',
|
||||
messages: chatMessages
|
||||
} : undefined
|
||||
}
|
||||
onNewChat={() => {
|
||||
setCopilotKey(prev => prev + 1);
|
||||
setCopilotMessages([]);
|
||||
setLoadingResponse(false);
|
||||
setLoadingMessage("Thinking...");
|
||||
setResponseError(null);
|
||||
}}
|
||||
messages={copilotMessages}
|
||||
setMessages={setCopilotMessages}
|
||||
loadingResponse={loadingResponse}
|
||||
setLoadingResponse={setLoadingResponse}
|
||||
loadingMessage={loadingMessage}
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
responseError={responseError}
|
||||
setResponseError={setResponseError}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>}
|
||||
</ResizablePanelGroup>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue