Workflow Copilot: review and approve/reject changes (#4559)
This commit is contained in:
committed by
GitHub
parent
cb2a72775d
commit
c0f361bb6e
@@ -16,6 +16,7 @@ import {
|
||||
WorkflowCopilotStreamResponseUpdate,
|
||||
WorkflowCopilotChatSender,
|
||||
WorkflowCopilotChatRequest,
|
||||
WorkflowCopilotClearProposedWorkflowRequest,
|
||||
} from "./workflowCopilotTypes";
|
||||
|
||||
interface ChatMessage {
|
||||
@@ -41,7 +42,12 @@ const formatChatTimestamp = (value: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const MessageItem = memo(({ message }: { message: ChatMessage }) => {
|
||||
interface MessageItemProps {
|
||||
message: ChatMessage;
|
||||
footer?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MessageItem = memo(({ message, footer }: MessageItemProps) => {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
@@ -55,6 +61,7 @@ const MessageItem = memo(({ message }: { message: ChatMessage }) => {
|
||||
<p className="whitespace-pre-wrap pr-3 text-sm text-slate-200">
|
||||
{message.content}
|
||||
</p>
|
||||
{footer ? <div className="mt-3 flex gap-2">{footer}</div> : null}
|
||||
{message.timestamp ? (
|
||||
<span className="pointer-events-none absolute bottom-2 right-2 rounded bg-slate-900/70 px-1.5 py-0.5 text-[10px] text-slate-400">
|
||||
{formatChatTimestamp(message.timestamp)}
|
||||
@@ -67,6 +74,10 @@ const MessageItem = memo(({ message }: { message: ChatMessage }) => {
|
||||
|
||||
interface WorkflowCopilotChatProps {
|
||||
onWorkflowUpdate?: (workflow: WorkflowApiResponse) => void;
|
||||
onReviewWorkflow?: (
|
||||
workflow: WorkflowApiResponse,
|
||||
clearPending: () => void,
|
||||
) => void;
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
onMessageCountChange?: (count: number) => void;
|
||||
@@ -117,12 +128,16 @@ const constrainPosition = (
|
||||
|
||||
export function WorkflowCopilotChat({
|
||||
onWorkflowUpdate,
|
||||
onReviewWorkflow,
|
||||
isOpen = true,
|
||||
onClose,
|
||||
onMessageCountChange,
|
||||
buttonRef,
|
||||
}: WorkflowCopilotChatProps = {}) {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [proposedWorkflow, setProposedWorkflow] =
|
||||
useState<WorkflowApiResponse | null>(null);
|
||||
const [autoAccept, setAutoAccept] = useState<boolean>(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [processingStatus, setProcessingStatus] = useState<string>("");
|
||||
@@ -184,9 +199,74 @@ export function WorkflowCopilotChat({
|
||||
const handleNewChat = () => {
|
||||
setMessages([]);
|
||||
setWorkflowCopilotChatId(null);
|
||||
setProposedWorkflow(null);
|
||||
setAutoAccept(false);
|
||||
hasScrolledOnLoad.current = false;
|
||||
};
|
||||
|
||||
const applyWorkflowUpdate = (workflow: WorkflowApiResponse): boolean => {
|
||||
if (!onWorkflowUpdate) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
onWorkflowUpdate(workflow);
|
||||
return true;
|
||||
} catch (updateError) {
|
||||
console.error("Failed to update workflow:", updateError);
|
||||
toast({
|
||||
title: "Update failed",
|
||||
description: "Failed to apply workflow changes. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAcceptWorkflow = (
|
||||
workflow: WorkflowApiResponse,
|
||||
alwaysAccept: boolean = false,
|
||||
) => {
|
||||
if (!applyWorkflowUpdate(workflow)) {
|
||||
return;
|
||||
}
|
||||
setProposedWorkflow(null);
|
||||
if (alwaysAccept) {
|
||||
setAutoAccept(true);
|
||||
}
|
||||
void clearProposedWorkflow(alwaysAccept);
|
||||
};
|
||||
|
||||
const handleRejectWorkflow = () => {
|
||||
setProposedWorkflow(null);
|
||||
void clearProposedWorkflow(false);
|
||||
};
|
||||
|
||||
const clearProposedWorkflow = async (autoAcceptValue: boolean) => {
|
||||
try {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
await client.post<WorkflowCopilotClearProposedWorkflowRequest>(
|
||||
"/workflow/copilot/clear-proposed-workflow",
|
||||
{
|
||||
workflow_copilot_chat_id: workflowCopilotChatId ?? "",
|
||||
auto_accept: autoAcceptValue,
|
||||
} as WorkflowCopilotClearProposedWorkflowRequest,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to clear proposed workflow:", error);
|
||||
toast({
|
||||
title: "Copilot update failed",
|
||||
description: autoAcceptValue
|
||||
? "Workflow was applied, but auto-accept did not update."
|
||||
: "Failed to clear copilot proposal. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReviewWorkflow = (workflow: WorkflowApiResponse) => {
|
||||
onReviewWorkflow?.(workflow, () => setProposedWorkflow(null));
|
||||
};
|
||||
|
||||
// Notify parent of message count changes
|
||||
useEffect(() => {
|
||||
if (onMessageCountChange) {
|
||||
@@ -214,6 +294,8 @@ export function WorkflowCopilotChat({
|
||||
if (!workflowPermanentId) {
|
||||
setMessages([]);
|
||||
setWorkflowCopilotChatId(null);
|
||||
setProposedWorkflow(null);
|
||||
setAutoAccept(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,6 +325,8 @@ export function WorkflowCopilotChat({
|
||||
);
|
||||
setMessages(historyMessages);
|
||||
setWorkflowCopilotChatId(response.data.workflow_copilot_chat_id);
|
||||
setProposedWorkflow(response.data.proposed_workflow ?? null);
|
||||
setAutoAccept(response.data.auto_accept ?? false);
|
||||
} catch (error) {
|
||||
console.error("Failed to load chat history:", error);
|
||||
} finally {
|
||||
@@ -306,6 +390,7 @@ export function WorkflowCopilotChat({
|
||||
|
||||
pendingMessageId.current = userMessageId;
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setProposedWorkflow(null);
|
||||
const messageContent = inputValue;
|
||||
setInputValue("");
|
||||
setIsLoading(true);
|
||||
@@ -398,10 +483,7 @@ export function WorkflowCopilotChat({
|
||||
setMessages((prev) =>
|
||||
prev.map((message) =>
|
||||
message.id === pendingId
|
||||
? {
|
||||
...message,
|
||||
timestamp: payload.timestamp,
|
||||
}
|
||||
? { ...message, timestamp: payload.timestamp }
|
||||
: message,
|
||||
),
|
||||
);
|
||||
@@ -420,19 +502,10 @@ export function WorkflowCopilotChat({
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, aiMessage]);
|
||||
|
||||
if (response.updated_workflow && onWorkflowUpdate) {
|
||||
try {
|
||||
onWorkflowUpdate(response.updated_workflow);
|
||||
} catch (updateError) {
|
||||
console.error("Failed to update workflow:", updateError);
|
||||
toast({
|
||||
title: "Update failed",
|
||||
description:
|
||||
"Failed to apply workflow changes. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
if (response.updated_workflow && autoAccept) {
|
||||
applyWorkflowUpdate(response.updated_workflow);
|
||||
} else {
|
||||
setProposedWorkflow(response.updated_workflow ?? null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -708,9 +781,52 @@ export function WorkflowCopilotChat({
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{messages.map((message) => (
|
||||
<MessageItem key={message.id} message={message} />
|
||||
))}
|
||||
{messages.map((message, index) => {
|
||||
const isLastMessage = index === messages.length - 1;
|
||||
const showProposedPanel = isLastMessage && proposedWorkflow;
|
||||
return (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
footer={
|
||||
showProposedPanel ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReviewWorkflow(proposedWorkflow)}
|
||||
className="rounded border border-blue-500/60 bg-blue-500/10 px-3 py-1 text-xs text-blue-100 hover:bg-blue-500/20"
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAcceptWorkflow(proposedWorkflow)}
|
||||
className="rounded bg-green-600 px-3 py-1 text-xs text-white hover:bg-green-700"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleAcceptWorkflow(proposedWorkflow, true)
|
||||
}
|
||||
className="rounded bg-emerald-600 px-3 py-1 text-xs text-white hover:bg-emerald-700"
|
||||
>
|
||||
Always accept
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRejectWorkflow}
|
||||
className="rounded bg-red-600 px-3 py-1 text-xs text-white hover:bg-red-700"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isLoading && (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white">
|
||||
|
||||
@@ -38,6 +38,13 @@ export interface WorkflowCopilotChatHistoryMessage {
|
||||
export interface WorkflowCopilotChatHistoryResponse {
|
||||
workflow_copilot_chat_id: string | null;
|
||||
chat_history: WorkflowCopilotChatHistoryMessage[];
|
||||
proposed_workflow?: WorkflowApiResponse | null;
|
||||
auto_accept?: boolean | null;
|
||||
}
|
||||
|
||||
export interface WorkflowCopilotClearProposedWorkflowRequest {
|
||||
workflow_copilot_chat_id: string;
|
||||
auto_accept: boolean;
|
||||
}
|
||||
|
||||
export type WorkflowCopilotStreamMessageType =
|
||||
|
||||
@@ -94,7 +94,11 @@ import {
|
||||
import { WorkflowHeader } from "./WorkflowHeader";
|
||||
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
|
||||
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
|
||||
import { WorkflowSettings } from "../types/workflowTypes";
|
||||
import {
|
||||
Parameter,
|
||||
WorkflowBlock,
|
||||
WorkflowSettings,
|
||||
} from "../types/workflowTypes";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import {
|
||||
nodeAdderNode,
|
||||
@@ -107,6 +111,7 @@ import {
|
||||
import { constructCacheKeyValue, getInitialParameters } from "./utils";
|
||||
import { WorkflowCopilotChat } from "../copilot/WorkflowCopilotChat";
|
||||
import { WorkflowCopilotButton } from "../copilot/WorkflowCopilotButton";
|
||||
import type { CopilotReviewStatus } from "./panels/WorkflowComparisonPanel";
|
||||
import "./workspace-styles.css";
|
||||
|
||||
const Constants = {
|
||||
@@ -981,6 +986,40 @@ function Workspace({
|
||||
}
|
||||
};
|
||||
|
||||
const applyWorkflowUpdate = (workflowData: WorkflowVersion) => {
|
||||
const settings: WorkflowSettings = {
|
||||
proxyLocation: workflowData.proxy_location ?? ProxyLocation.Residential,
|
||||
webhookCallbackUrl: workflowData.webhook_callback_url || "",
|
||||
persistBrowserSession: workflowData.persist_browser_session ?? false,
|
||||
model: workflowData.model ?? null,
|
||||
maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3,
|
||||
extraHttpHeaders: workflowData.extra_http_headers
|
||||
? JSON.stringify(workflowData.extra_http_headers)
|
||||
: null,
|
||||
runWith: workflowData.run_with ?? null,
|
||||
scriptCacheKey: workflowData.cache_key ?? null,
|
||||
aiFallback: workflowData.ai_fallback ?? true,
|
||||
runSequentially: workflowData.run_sequentially ?? false,
|
||||
sequentialKey: workflowData.sequential_key ?? null,
|
||||
finallyBlockLabel:
|
||||
workflowData.workflow_definition?.finally_block_label ?? null,
|
||||
};
|
||||
|
||||
const elements = getElements(
|
||||
workflowData.workflow_definition.blocks,
|
||||
settings,
|
||||
true,
|
||||
);
|
||||
|
||||
setNodes(elements.nodes);
|
||||
setEdges(elements.edges);
|
||||
|
||||
const initialParameters = getInitialParameters(workflowData);
|
||||
useWorkflowParametersStore.getState().setParameters(initialParameters);
|
||||
|
||||
workflowChangesStore.setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSelectState = (selectedVersion: WorkflowVersion) => {
|
||||
// Close panels
|
||||
setWorkflowPanelState({
|
||||
@@ -1239,7 +1278,9 @@ function Workspace({
|
||||
<div
|
||||
className="absolute left-6 top-[6rem]"
|
||||
style={{
|
||||
width: "calc(100% - 32rem)",
|
||||
width: workflowPanelState.active
|
||||
? "calc(100% - 32rem)"
|
||||
: "calc(100% - 3rem)",
|
||||
height: "calc(100vh - 11rem)",
|
||||
}}
|
||||
>
|
||||
@@ -1248,6 +1289,10 @@ function Workspace({
|
||||
version1={workflowPanelState.data.version1}
|
||||
version2={workflowPanelState.data.version2}
|
||||
onSelectState={handleSelectState}
|
||||
mode={workflowPanelState.data.mode}
|
||||
onCopilotReviewClose={
|
||||
workflowPanelState.data.onCopilotReviewClose
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1698,43 +1743,109 @@ function Workspace({
|
||||
onClose={() => setIsCopilotOpen(false)}
|
||||
onMessageCountChange={setCopilotMessageCount}
|
||||
buttonRef={copilotButtonRef}
|
||||
onReviewWorkflow={(pendingWorkflow, clearPending) => {
|
||||
const saveData = workflowChangesStore.getSaveData?.();
|
||||
if (!saveData) return;
|
||||
|
||||
// Construct fake WorkflowVersion for current state
|
||||
const currentVersion: WorkflowVersion = {
|
||||
workflow_id: saveData.workflow.workflow_id,
|
||||
organization_id: "",
|
||||
is_saved_task: saveData.workflow.is_saved_task ?? false,
|
||||
is_template: false,
|
||||
title: "Current",
|
||||
workflow_permanent_id: saveData.workflow.workflow_permanent_id,
|
||||
version: saveData.workflow.version ?? 0,
|
||||
description: saveData.workflow.description ?? "",
|
||||
workflow_definition: {
|
||||
parameters: saveData.parameters as unknown as Parameter[],
|
||||
blocks: saveData.blocks as unknown as WorkflowBlock[],
|
||||
finally_block_label: saveData.settings.finallyBlockLabel,
|
||||
},
|
||||
proxy_location: saveData.settings.proxyLocation,
|
||||
webhook_callback_url: saveData.settings.webhookCallbackUrl,
|
||||
extra_http_headers: saveData.settings.extraHttpHeaders
|
||||
? JSON.parse(saveData.settings.extraHttpHeaders)
|
||||
: null,
|
||||
persist_browser_session: saveData.settings.persistBrowserSession,
|
||||
model: saveData.settings.model,
|
||||
totp_verification_url: saveData.workflow.totp_verification_url,
|
||||
totp_identifier: null,
|
||||
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
|
||||
status: saveData.workflow.status,
|
||||
created_at: new Date().toISOString(),
|
||||
modified_at: new Date().toISOString(),
|
||||
deleted_at: null,
|
||||
run_with: saveData.settings.runWith,
|
||||
cache_key: saveData.settings.scriptCacheKey,
|
||||
ai_fallback: saveData.settings.aiFallback,
|
||||
run_sequentially: saveData.settings.runSequentially,
|
||||
sequential_key: saveData.settings.sequentialKey,
|
||||
folder_id: null,
|
||||
import_error: null,
|
||||
};
|
||||
|
||||
// Construct fake WorkflowVersion for pending copilot suggestion
|
||||
const pendingVersion: WorkflowVersion = {
|
||||
...pendingWorkflow,
|
||||
title: "Copilot Suggestion",
|
||||
};
|
||||
|
||||
// Handle copilot review close with status
|
||||
const handleCopilotReviewClose = (status: CopilotReviewStatus) => {
|
||||
if (status === "approve") {
|
||||
try {
|
||||
applyWorkflowUpdate(pendingWorkflow);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to apply copilot workflow",
|
||||
error,
|
||||
pendingWorkflow,
|
||||
);
|
||||
toast({
|
||||
title: "Update failed",
|
||||
description:
|
||||
"Failed to apply workflow update. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Close the panel and reopen copilot chat
|
||||
setWorkflowPanelState({
|
||||
active: false,
|
||||
content: "history",
|
||||
data: {
|
||||
showComparison: false,
|
||||
version1: undefined,
|
||||
version2: undefined,
|
||||
},
|
||||
});
|
||||
setIsCopilotOpen(true);
|
||||
|
||||
// Clear pending for approve and reject, but not for close
|
||||
if (status !== "close") {
|
||||
clearPending();
|
||||
}
|
||||
};
|
||||
|
||||
// Hide chat and show comparison
|
||||
setIsCopilotOpen(false);
|
||||
setWorkflowPanelState({
|
||||
active: false,
|
||||
content: "history",
|
||||
data: {
|
||||
version1: currentVersion,
|
||||
version2: pendingVersion,
|
||||
showComparison: true,
|
||||
mode: "copilot",
|
||||
onCopilotReviewClose: handleCopilotReviewClose,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onWorkflowUpdate={(workflowData) => {
|
||||
try {
|
||||
const settings: WorkflowSettings = {
|
||||
proxyLocation:
|
||||
workflowData.proxy_location ?? ProxyLocation.Residential,
|
||||
webhookCallbackUrl: workflowData.webhook_callback_url || "",
|
||||
persistBrowserSession:
|
||||
workflowData.persist_browser_session ?? false,
|
||||
model: workflowData.model ?? null,
|
||||
maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3,
|
||||
extraHttpHeaders: workflowData.extra_http_headers
|
||||
? JSON.stringify(workflowData.extra_http_headers)
|
||||
: null,
|
||||
runWith: workflowData.run_with ?? null,
|
||||
scriptCacheKey: workflowData.cache_key ?? null,
|
||||
aiFallback: workflowData.ai_fallback ?? true,
|
||||
runSequentially: workflowData.run_sequentially ?? false,
|
||||
sequentialKey: workflowData.sequential_key ?? null,
|
||||
finallyBlockLabel:
|
||||
workflowData.workflow_definition?.finally_block_label ?? null,
|
||||
};
|
||||
|
||||
const elements = getElements(
|
||||
workflowData.workflow_definition.blocks,
|
||||
settings,
|
||||
true,
|
||||
);
|
||||
|
||||
setNodes(elements.nodes);
|
||||
setEdges(elements.edges);
|
||||
|
||||
const initialParameters = getInitialParameters(workflowData);
|
||||
useWorkflowParametersStore
|
||||
.getState()
|
||||
.setParameters(initialParameters);
|
||||
|
||||
workflowChangesStore.setHasChanges(true);
|
||||
applyWorkflowUpdate(workflowData);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to parse and apply workflow",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
@@ -22,10 +23,16 @@ type BlockComparison = {
|
||||
identifier: string;
|
||||
};
|
||||
|
||||
type ComparisonMode = "history" | "copilot";
|
||||
|
||||
export type CopilotReviewStatus = "approve" | "reject" | "close";
|
||||
|
||||
type Props = {
|
||||
version1: WorkflowVersion;
|
||||
version2: WorkflowVersion;
|
||||
onSelectState?: (version: WorkflowVersion) => void;
|
||||
mode?: ComparisonMode;
|
||||
onCopilotReviewClose?: (status: CopilotReviewStatus) => void;
|
||||
};
|
||||
|
||||
// Mapping from WorkflowBlock.block_type to ReactFlow node.type
|
||||
@@ -255,7 +262,13 @@ function WorkflowComparisonRenderer({
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
||||
function WorkflowComparisonPanel({
|
||||
version1,
|
||||
version2,
|
||||
onSelectState,
|
||||
mode = "history",
|
||||
onCopilotReviewClose,
|
||||
}: Props) {
|
||||
const comparisons = useMemo(() => {
|
||||
const blocks1 = version1?.workflow_definition?.blocks || [];
|
||||
const blocks2 = version2?.workflow_definition?.blocks || [];
|
||||
@@ -265,6 +278,20 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
||||
version2?.workflow_definition?.blocks,
|
||||
]);
|
||||
|
||||
// ESC key handler for copilot mode - close without rejecting
|
||||
useEffect(() => {
|
||||
if (mode !== "copilot" || !onCopilotReviewClose) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onCopilotReviewClose("close");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [mode, onCopilotReviewClose]);
|
||||
|
||||
// Statistics
|
||||
const stats = useMemo(
|
||||
() => ({
|
||||
@@ -321,7 +348,18 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col rounded-lg bg-slate-elevation2">
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 p-4 pb-3">
|
||||
<div className="relative flex-shrink-0 p-4 pb-3">
|
||||
{/* Close button for copilot mode */}
|
||||
{mode === "copilot" && onCopilotReviewClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCopilotReviewClose("close")}
|
||||
className="absolute right-4 top-4 rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-slate-200"
|
||||
title="Close (Esc)"
|
||||
>
|
||||
<Cross2Icon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
{/* 3x3 Grid Layout */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* Row 1: Workflow Names and Title */}
|
||||
@@ -329,17 +367,21 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
||||
{version1.title}
|
||||
</h2>
|
||||
<h3 className="text-center text-lg font-medium text-muted-foreground">
|
||||
Version Comparison
|
||||
{mode === "copilot" ? "Copilot Review" : "Version Comparison"}
|
||||
</h3>
|
||||
<h2 className="text-center text-xl font-semibold">
|
||||
{version2.title}
|
||||
</h2>
|
||||
|
||||
{/* Row 2: Version Details and Statistics */}
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
[Version {version1.version}] •{" "}
|
||||
{new Date(version1.modified_at).toLocaleDateString()}
|
||||
</div>
|
||||
{mode === "history" ? (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
[Version {version1.version}] •{" "}
|
||||
{new Date(version1.modified_at).toLocaleDateString()}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
<div className="flex justify-center gap-3 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-3 w-3 rounded-full bg-green-300"></div>
|
||||
@@ -358,14 +400,18 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
||||
<span>Removed ({stats.removed})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
[Version {version2.version}] •{" "}
|
||||
{new Date(version2.modified_at).toLocaleDateString()}
|
||||
</div>
|
||||
{mode === "history" ? (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
[Version {version2.version}] •{" "}
|
||||
{new Date(version2.modified_at).toLocaleDateString()}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{/* Row 3: Select Buttons */}
|
||||
{/* Row 3: Action Buttons */}
|
||||
<div className="flex justify-center">
|
||||
{onSelectState && (
|
||||
{mode === "history" && onSelectState && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onSelectState(version1)}
|
||||
@@ -375,9 +421,29 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div></div>
|
||||
<div className="flex justify-center gap-2">
|
||||
{mode === "copilot" && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => onCopilotReviewClose?.("reject")}
|
||||
className="text-xs"
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onCopilotReviewClose?.("approve")}
|
||||
className="bg-green-600 text-xs hover:bg-green-700"
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
{onSelectState && (
|
||||
{mode === "history" && onSelectState && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onSelectState(version2)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { WorkflowVersion } from "@/routes/workflows/hooks/useWorkflowVersionsQuery";
|
||||
import { CopilotReviewStatus } from "@/routes/workflows/editor/panels/WorkflowComparisonPanel";
|
||||
|
||||
export type BranchContext = {
|
||||
conditionalNodeId: string;
|
||||
@@ -27,6 +28,8 @@ type WorkflowPanelState = {
|
||||
version1?: WorkflowVersion;
|
||||
version2?: WorkflowVersion;
|
||||
showComparison?: boolean;
|
||||
mode?: "history" | "copilot";
|
||||
onCopilotReviewClose?: (status: CopilotReviewStatus) => void;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user