Make copilot collapsible

This commit is contained in:
akhisud3195 2025-02-17 16:00:25 +05:30
parent c4e6f1d607
commit b8ac0482fc
2 changed files with 117 additions and 37 deletions

View file

@ -174,23 +174,35 @@ function App({
workflow, workflow,
dispatch, dispatch,
chatContext=undefined, chatContext=undefined,
messages,
setMessages,
loadingResponse,
setLoadingResponse,
loadingMessage,
setLoadingMessage,
responseError,
setResponseError,
}: { }: {
projectId: string; projectId: string;
workflow: z.infer<typeof Workflow>; workflow: z.infer<typeof Workflow>;
dispatch: (action: WorkflowDispatch) => void; dispatch: (action: WorkflowDispatch) => void;
chatContext?: z.infer<typeof CopilotChatContext>; 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 messagesEndRef = useRef<HTMLDivElement>(null);
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({}); const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
const [discardContext, setDiscardContext] = useState(false); const [discardContext, setDiscardContext] = useState(false);
const [lastRequest, setLastRequest] = useState<unknown | null>(null); const [lastRequest, setLastRequest] = useState<unknown | null>(null);
const [lastResponse, setLastResponse] = 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(() => { useEffect(() => {
setLoadingMessage("Thinking"); setLoadingMessage("Thinking");
if (!loadingResponse) return; if (!loadingResponse) return;
@ -210,7 +222,7 @@ function App({
}, 4000); }, 4000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [loadingResponse, messages]); }, [loadingResponse, setLoadingMessage]);
// Reset discardContext when chatContext changes // Reset discardContext when chatContext changes
useEffect(() => { useEffect(() => {
@ -328,7 +340,7 @@ function App({
} }
}, [dispatch, appliedChanges, messages]); }, [dispatch, appliedChanges, messages]);
// get copilot response // Second useEffect for copilot response
useEffect(() => { useEffect(() => {
let ignore = false; let ignore = false;
@ -383,7 +395,16 @@ function App({
return () => { return () => {
ignore = true; ignore = true;
}; };
}, [messages, projectId, responseError, workflow, effectiveContext]); }, [
messages,
projectId,
responseError,
workflow,
effectiveContext,
setLoadingResponse,
setMessages,
setResponseError
]);
function handleCopyChat() { function handleCopyChat() {
const jsonString = JSON.stringify({ const jsonString = JSON.stringify({
@ -483,18 +504,30 @@ export function Copilot({
workflow, workflow,
chatContext=undefined, chatContext=undefined,
dispatch, dispatch,
onNewChat,
messages,
setMessages,
loadingResponse,
setLoadingResponse,
loadingMessage,
setLoadingMessage,
responseError,
setResponseError,
}: { }: {
projectId: string; projectId: string;
workflow: z.infer<typeof Workflow>; workflow: z.infer<typeof Workflow>;
chatContext?: z.infer<typeof CopilotChatContext>; chatContext?: z.infer<typeof CopilotChatContext>;
dispatch: (action: WorkflowDispatch) => void; 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 ( return (
<Pane fancy title="Copilot" actions={[ <Pane fancy title="Copilot" actions={[
<ActionButton <ActionButton
@ -505,18 +538,24 @@ export function Copilot({
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 12h14m-7 7V5" /> <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 12h14m-7 7V5" />
</svg> </svg>
} }
onClick={handleNewChat} onClick={onNewChat}
> >
Ask New
</ActionButton> </ActionButton>
]}> ]}>
<App <App
key={key}
projectId={projectId} projectId={projectId}
workflow={workflow} workflow={workflow}
dispatch={dispatch} dispatch={dispatch}
chatContext={chatContext} chatContext={chatContext}
messages={messages}
setMessages={setMessages}
loadingResponse={loadingResponse}
setLoadingResponse={setLoadingResponse}
loadingMessage={loadingMessage}
setLoadingMessage={setLoadingMessage}
responseError={responseError}
setResponseError={setResponseError}
/> />
</Pane> </Pane>
); );

View file

@ -26,8 +26,9 @@ import { apiV1 } from "rowboat-shared";
import { publishWorkflow, renameWorkflow, saveWorkflow } from "../../../actions/workflow_actions"; import { publishWorkflow, renameWorkflow, saveWorkflow } from "../../../actions/workflow_actions";
import { PublishedBadge } from "./published_badge"; import { PublishedBadge } from "./published_badge";
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons"; 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 { EntityList } from "./entity_list";
import { CopilotMessage } from "../../../lib/types/copilot_types";
enablePatches(); enablePatches();
@ -524,6 +525,13 @@ export function WorkflowEditor({
const saving = useRef(false); const saving = useRef(false);
const isLive = state.present.workflow._id == state.present.publishedWorkflowId; const isLive = state.present.workflow._id == state.present.publishedWorkflowId;
const [showCopySuccess, setShowCopySuccess] = useState(false); 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}`); console.log(`workflow editor chat key: ${state.present.chatKey}`);
@ -767,6 +775,13 @@ export function WorkflowEditor({
> >
<RedoIcon size={16} /> <RedoIcon size={16} />
</button> </button>
<button
className="p-1 text-blue-600 hover:text-blue-800"
title="Toggle Copilot"
onClick={() => setShowCopilot(!showCopilot)}
>
<Sparkles size={16} />
</button>
</>} </>}
</div> </div>
</div> </div>
@ -792,7 +807,11 @@ export function WorkflowEditor({
/> />
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel minSize={20} defaultSize={60} className="overflow-auto"> <ResizablePanel
minSize={20}
defaultSize={showCopilot ? 85 - copilotWidth : 85}
className="overflow-auto"
>
<ChatApp <ChatApp
key={'' + state.present.chatKey} key={'' + state.present.chatKey}
hidden={state.present.selection !== null} hidden={state.present.selection !== null}
@ -826,23 +845,45 @@ export function WorkflowEditor({
handleClose={handleUnselectPrompt} handleClose={handleUnselectPrompt}
/>} />}
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> {showCopilot && <>
<ResizablePanel minSize={10} defaultSize={25}> <ResizableHandle />
<Copilot <ResizablePanel
projectId={state.present.workflow.projectId} minSize={10}
workflow={state.present.workflow} defaultSize={copilotWidth}
dispatch={dispatch} onResize={(size) => setCopilotWidth(size)}
chatContext={ >
state.present.selection ? { <Copilot
type: state.present.selection.type, key={copilotKey}
name: state.present.selection.name projectId={state.present.workflow.projectId}
} : chatMessages.length > 0 ? { workflow={state.present.workflow}
type: 'chat', dispatch={dispatch}
messages: chatMessages chatContext={
} : undefined state.present.selection ? {
} type: state.present.selection.type,
/> name: state.present.selection.name
</ResizablePanel> } : 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> </ResizablePanelGroup>
</div>; </div>;
} }