mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
Make copilot collapsible
This commit is contained in:
parent
c4e6f1d607
commit
b8ac0482fc
2 changed files with 117 additions and 37 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue