Display copilot and playground as toggles

This commit is contained in:
akhisud3195 2025-09-03 17:59:08 +04:00
parent 3d36884c73
commit 251198225b
8 changed files with 225 additions and 171 deletions

View file

@ -269,7 +269,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
toolQuery={toolQuery}
/>
</div>
<div className="shrink-0 px-1">
<div className="shrink-0 px-0 pb-0">
{responseError && (
<div className="mb-4 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex gap-2 justify-between items-center text-sm">
<p className="text-red-600 dark:text-red-400">{responseError}</p>
@ -322,6 +322,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
dispatch: (action: WorkflowDispatch) => void;
isInitialState?: boolean;
dataSources?: z.infer<typeof DataSource>[];
activePanel: 'playground' | 'copilot';
onTogglePanel: () => void;
}>(({
projectId,
workflow,
@ -329,6 +331,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
dispatch,
isInitialState = false,
dataSources,
activePanel,
onTogglePanel,
}, ref) => {
const [copilotKey, setCopilotKey] = useState(0);
const [showCopySuccess, setShowCopySuccess] = useState(false);
@ -365,8 +369,34 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
<Panel
variant="copilot"
tourTarget="copilot"
icon={<Sparkles className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />}
title="Skipper"
title={
<div className="flex items-center">
<div className="flex items-center gap-2 rounded-lg p-1 bg-blue-50/70 dark:bg-blue-900/30">
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'copilot'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base"></span>
Build
</button>
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'playground'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base">💬</span>
Test
</button>
</div>
</div>
}
subtitle="Build your assistant"
rightActions={
<div className="flex items-center gap-2">

View file

@ -17,6 +17,8 @@ export function App({
onPanelClick,
triggerCopilotChat,
isLiveWorkflow,
activePanel,
onTogglePanel,
}: {
hidden?: boolean;
projectId: string;
@ -25,6 +27,8 @@ export function App({
onPanelClick?: () => void;
triggerCopilotChat?: (message: string) => void;
isLiveWorkflow: boolean;
activePanel: 'playground' | 'copilot';
onTogglePanel: () => void;
}) {
const [counter, setCounter] = useState<number>(0);
const [showDebugMessages, setShowDebugMessages] = useState<boolean>(true);
@ -56,8 +60,34 @@ export function App({
className={`${hidden ? 'hidden' : 'block'}`}
variant="playground"
tourTarget="playground"
icon={<MessageCircle className="w-5 h-5 text-blue-600 dark:text-blue-400" />}
title="Chat"
title={
<div className="flex items-center">
<div className="flex items-center gap-2 rounded-lg p-1 bg-blue-50/70 dark:bg-blue-900/30">
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'copilot'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base"></span>
Build
</button>
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'playground'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base">💬</span>
Test
</button>
</div>
</div>
}
subtitle="Chat with your assistant"
rightActions={
<div className="flex items-center gap-2">

View file

@ -425,7 +425,7 @@ export function Chat({
<ChevronDownIcon className="w-5 h-5" strokeWidth={2.2} />
</button>
)}
<div className="bg-white dark:bg-zinc-900 pt-4 pb-2">
<div className="bg-white dark:bg-zinc-900 pt-4 pb-6">
{showSuccessMessage && (
<div className="mb-4 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800
rounded-lg flex gap-2 justify-between items-center">

View file

@ -14,14 +14,14 @@ interface TopBarProps {
showCopySuccess: boolean;
canUndo: boolean;
canRedo: boolean;
showCopilot: boolean;
activePanel: 'playground' | 'copilot';
onUndo: () => void;
onRedo: () => void;
onDownloadJSON: () => void;
onPublishWorkflow: () => void;
onChangeMode: (mode: 'draft' | 'live') => void;
onRevertToLive: () => void;
onToggleCopilot: () => void;
onTogglePanel: () => void;
}
export function TopBar({
@ -34,14 +34,14 @@ export function TopBar({
showCopySuccess,
canUndo,
canRedo,
showCopilot,
activePanel,
onUndo,
onRedo,
onDownloadJSON,
onPublishWorkflow,
onChangeMode,
onRevertToLive,
onToggleCopilot,
onTogglePanel,
}: TopBarProps) {
const router = useRouter();
const params = useParams();
@ -70,47 +70,29 @@ export function TopBar({
classNames={{
base: "max-w-xs",
input: "text-base font-semibold px-2",
inputWrapper: "min-h-[28px] h-[28px] border-gray-200 dark:border-gray-700 px-0"
inputWrapper: "min-h-[36px] h-[36px] border-gray-200 dark:border-gray-700 px-0"
}}
/>
</div>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
<div className="flex items-center gap-2">
{publishing && <Spinner size="sm" />}
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<RadioIcon size={16} />
Live workflow
</div>}
{!isLive && <div className="bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<PenLine size={16} />
Draft workflow
</div>}
{/* Download JSON icon button, with tooltip, to the left of the menu */}
<Tooltip content="Download Assistant JSON">
<button
onClick={onDownloadJSON}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Download JSON"
type="button"
>
<DownloadIcon size={20} />
</button>
</Tooltip>
</div>
{/* Show divider and CTA only in live view */}
{isLive && <div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>}
{isLive ? (
<Button
variant="solid"
size="md"
onPress={() => onChangeMode('draft')}
className="gap-2 px-4 bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-300 font-medium text-sm border border-gray-200 dark:border-gray-600 shadow-sm"
startContent={<PenLine size={16} />}
>
Switch to draft
</Button>
) : null}
</div>
{showCopySuccess && <div className="flex items-center gap-2">
<div className="text-green-500">Copied to clipboard</div>
</div>}
<div className="flex items-center gap-2">
{isLive && <div className="flex items-center gap-2 absolute left-1/2 transform -translate-x-1/2">
<div className="bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
<AlertTriangle size={16} />
This version is locked. Changes applied will not be reflected.
</div>
</div>}
{!isLive && <>
<button
@ -132,44 +114,66 @@ export function TopBar({
</>}
{/* Deploy CTA - always visible */}
<div className="flex">
<div className="flex items-center gap-3">
{isLive ? (
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="md"
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
startContent={<Plug size={16} />}
>
Use Assistant
<ChevronDownIcon size={14} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Assistant access options">
<DropdownItem
key="api-sdk"
startContent={<SettingsIcon size={16} />}
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }}
>
API & SDK Settings
</DropdownItem>
<DropdownItem
key="manage-triggers"
startContent={<ZapIcon size={16} />}
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }}
<>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="md"
className="gap-2 px-4 bg-blue-50 hover:bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 dark:text-blue-400 font-semibold text-sm border border-blue-200 dark:border-blue-700 shadow-sm"
startContent={<Plug size={16} />}
>
Manage Triggers
</DropdownItem>
</DropdownMenu>
</Dropdown>
Use Assistant
<ChevronDownIcon size={14} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Assistant access options">
<DropdownItem
key="api-sdk"
startContent={<SettingsIcon size={16} />}
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }}
>
API & SDK Settings
</DropdownItem>
<DropdownItem
key="manage-triggers"
startContent={<ZapIcon size={16} />}
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }}
>
Manage Triggers
</DropdownItem>
</DropdownMenu>
</Dropdown>
{/* Live workflow label moved here */}
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
<div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<RadioIcon size={16} />
Live workflow
</div>
<Tooltip content="Download Assistant JSON">
<button
onClick={onDownloadJSON}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Download JSON"
type="button"
>
<DownloadIcon size={20} />
</button>
</Tooltip>
</div>
</>
) : (
<>
<div className="flex">
<Button
variant="solid"
size="md"
onPress={onPublishWorkflow}
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm rounded-r-none"
className="gap-2 px-4 bg-green-100 hover:bg-green-200 text-green-800 font-semibold text-sm rounded-r-none border border-green-300 shadow-sm"
startContent={<RocketIcon size={16} />}
data-tour-target="deploy"
>
@ -180,7 +184,7 @@ export function TopBar({
<Button
variant="solid"
size="md"
className="min-w-0 px-2 bg-green-600 hover:bg-green-700 border-l-1 border-green-500 text-white font-semibold text-sm rounded-l-none"
className="min-w-0 px-2 bg-green-100 hover:bg-green-200 text-green-800 rounded-l-none border border-l-0 border-green-300 shadow-sm"
>
<ChevronDownIcon size={14} />
</Button>
@ -203,31 +207,34 @@ export function TopBar({
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
{/* Moved draft/live labels and download button here */}
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<RadioIcon size={16} />
Live workflow
</div>}
{!isLive && <div className="bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<PenLine size={16} />
Draft workflow
</div>}
<Tooltip content="Download Assistant JSON">
<button
onClick={onDownloadJSON}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Download JSON"
type="button"
>
<DownloadIcon size={20} />
</button>
</Tooltip>
</div>
</>
)}
</div>
{isLive && <div className="flex items-center gap-2">
<Button
variant="solid"
size="md"
onPress={() => onChangeMode('draft')}
className="gap-2 px-4 bg-gray-600 hover:bg-gray-700 text-white font-semibold text-sm"
startContent={<PenLine size={16} />}
>
Switch to draft
</Button>
</div>}
{!isLive && <Button
variant="solid"
size="md"
onPress={onToggleCopilot}
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
startContent={showCopilot ? null : <span className="text-indigo-300"></span>}
>
{showCopilot ? "Hide Skipper" : "Skipper"}
</Button>}
</div>
</div>
</div>

View file

@ -1175,7 +1175,7 @@ export const EntityList = forwardRef<
tourTarget="entity-prompts"
className={clsx(
"h-full",
!expandedPanels.prompts && "h-[53px]!"
!expandedPanels.prompts && "h-[61px]!"
)}
title={
<div className={`${headerClasses} rounded-md transition-colors h-full`}>
@ -1208,7 +1208,7 @@ export const EntityList = forwardRef<
}
>
{expandedPanels.prompts && (
<div className="h-[calc(100%-53px)] overflow-y-auto">
<div className="h-[calc(100%-61px)] overflow-y-auto">
<div className="p-2">
{prompts.length > 0 ? (
<div className="space-y-1">
@ -2116,4 +2116,4 @@ function AddVariableModal({ isOpen, onClose, onConfirm, initialName, initialValu
</ModalContent>
</Modal>
);
}
}

View file

@ -965,8 +965,7 @@ export function WorkflowEditor({
const saveQueue = useRef<z.infer<typeof Workflow>[]>([]);
const saving = useRef(false);
const [showCopySuccess, setShowCopySuccess] = useState(false);
const [showCopilot, setShowCopilot] = useState(true);
const [copilotWidth, setCopilotWidth] = useState<number>(PANEL_RATIOS.copilot);
const [activePanel, setActivePanel] = useState<'playground' | 'copilot'>('copilot');
const [isInitialState, setIsInitialState] = useState(true);
const [showTour, setShowTour] = useState(true);
const copilotRef = useRef<{ handleUserMessage: (message: string) => void }>(null);
@ -1010,7 +1009,7 @@ export function WorkflowEditor({
// Function to trigger copilot chat
const triggerCopilotChat = useCallback((message: string) => {
setShowCopilot(true);
setActivePanel('copilot');
// Small delay to ensure copilot is mounted
setTimeout(() => {
copilotRef.current?.handleUserMessage(message);
@ -1028,14 +1027,14 @@ export function WorkflowEditor({
const prompt = localStorage.getItem(`project_prompt_${projectId}`);
console.log('init project prompt', prompt);
if (prompt) {
setShowCopilot(true);
setActivePanel('copilot');
}
}, [projectId]);
// Hide copilot when switching to live mode
// Switch to playground when switching to live mode
useEffect(() => {
if (isLive) {
setShowCopilot(false);
setActivePanel('playground');
}
}, [isLive]);
@ -1397,14 +1396,14 @@ export function WorkflowEditor({
showCopySuccess={showCopySuccess}
canUndo={state.currentIndex > 0}
canRedo={state.currentIndex < state.patches.length}
showCopilot={showCopilot}
activePanel={activePanel}
onUndo={() => dispatch({ type: "undo" })}
onRedo={() => dispatch({ type: "redo" })}
onDownloadJSON={handleDownloadJSON}
onPublishWorkflow={handlePublishWorkflow}
onChangeMode={onChangeMode}
onRevertToLive={handleRevertToLive}
onToggleCopilot={() => setShowCopilot(!showCopilot)}
onTogglePanel={() => setActivePanel(activePanel === 'playground' ? 'copilot' : 'playground')}
/>
{/* Content Area */}
@ -1465,19 +1464,48 @@ export function WorkflowEditor({
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
<ResizablePanel
minSize={20}
defaultSize={showCopilot ? PANEL_RATIOS.chatApp : PANEL_RATIOS.chatApp + PANEL_RATIOS.copilot}
defaultSize={PANEL_RATIOS.chatApp + PANEL_RATIOS.copilot}
className="overflow-auto"
>
<ChatApp
key={'' + state.present.chatKey}
hidden={state.present.selection !== null}
projectId={projectId}
workflow={state.present.workflow}
messageSubscriber={updateChatMessages}
onPanelClick={handlePlaygroundClick}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLive}
/>
<div className={(activePanel === 'playground' && state.present.selection === null) ? 'block h-full' : 'hidden h-full'}>
<ChatApp
key={'' + state.present.chatKey}
hidden={state.present.selection !== null}
projectId={projectId}
workflow={state.present.workflow}
messageSubscriber={updateChatMessages}
onPanelClick={handlePlaygroundClick}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLive}
activePanel={activePanel}
onTogglePanel={() => setActivePanel(activePanel === 'playground' ? 'copilot' : 'playground')}
/>
</div>
<div className={(activePanel === 'copilot' && state.present.selection === null) ? 'block h-full' : 'hidden h-full'}>
<Copilot
ref={copilotRef}
projectId={projectId}
workflow={state.present.workflow}
dispatch={dispatch}
chatContext={
state.present.selection &&
(state.present.selection.type === "agent" ||
state.present.selection.type === "tool" ||
state.present.selection.type === "prompt")
? {
type: state.present.selection.type,
name: state.present.selection.name
}
: chatMessages.length > 0
? { type: 'chat', messages: chatMessages }
: undefined
}
isInitialState={isInitialState}
dataSources={dataSources}
activePanel={activePanel}
onTogglePanel={() => setActivePanel(activePanel === 'playground' ? 'copilot' : 'playground')}
/>
</div>
{state.present.selection?.type === "agent" && <AgentConfig
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}`}
projectId={projectId}
@ -1563,38 +1591,6 @@ export function WorkflowEditor({
</Panel>
)}
</ResizablePanel>
{showCopilot && (
<>
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
<ResizablePanel
minSize={10}
defaultSize={PANEL_RATIOS.copilot}
onResize={(size) => setCopilotWidth(size)}
>
<Copilot
ref={copilotRef}
projectId={projectId}
workflow={state.present.workflow}
dispatch={dispatch}
chatContext={
state.present.selection &&
(state.present.selection.type === "agent" ||
state.present.selection.type === "tool" ||
state.present.selection.type === "prompt")
? {
type: state.present.selection.type,
name: state.present.selection.name
}
: chatMessages.length > 0
? { type: 'chat', messages: chatMessages }
: undefined
}
isInitialState={isInitialState}
dataSources={dataSources}
/>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
{USE_PRODUCT_TOUR && showTour && (
<ProductTour

View file

@ -85,11 +85,11 @@ export function ComposeBoxCopilot({
group-hover:opacity-100 transition-opacity">
Press + Enter to send
</div>
{/* Outer container with padding */}
<div className="rounded-2xl border-[1.5px] border-gray-200 dark:border-[#2a2d31] p-3 relative
{/* Outer container without external padding; textarea grows to fill */}
<div className="rounded-2xl border-[1.5px] border-gray-200 dark:border-[#2a2d31] relative
bg-white dark:bg-[#1e2023] flex items-end gap-2">
{/* Textarea */}
<div className="flex-1">
<div className="flex-1 p-3">
<Textarea
ref={textareaRef}
value={input}

View file

@ -83,12 +83,15 @@ export function Panel({
>
<div
className={clsx(
"shrink-0 border-b relative",
variant === 'copilot' ? "border-zinc-300 dark:border-zinc-700" : "border-zinc-100 dark:border-zinc-800",
// For copilot and playground, mimic TopBar appearance
(variant === 'copilot' || variant === 'playground')
? "shrink-0 relative rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm border border-zinc-200 dark:border-zinc-800 px-0 pt-0 pb-2 mx-0 mt-0 mb-2 flex items-center justify-between"
: "shrink-0 border-b relative",
(variant !== 'copilot' && variant !== 'playground') && "border-zinc-100 dark:border-zinc-800",
{
"flex flex-col gap-3 px-4 py-3": variant === 'projects',
"flex items-center justify-between h-[53px] p-3": isEntityList,
"flex items-center justify-between px-6 py-3": !isEntityList && variant !== 'projects'
"flex items-center justify-between px-6 py-3": !isEntityList && variant !== 'projects' && variant !== 'copilot' && variant !== 'playground'
}
)}
>
@ -102,39 +105,27 @@ export function Panel({
</div>}
</>
) : variant === 'copilot' ? (
<>
<div className="w-full flex items-center justify-between px-3 pt-2">
<div className="flex items-center gap-2">
{icon && icon}
<div className="flex flex-col">
<div className="font-semibold text-zinc-700 dark:text-zinc-300">
{title}
</div>
{subtitle && (
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{subtitle}
</div>
)}
</div>
</div>
{rightActions}
</>
</div>
) : variant === 'playground' ? (
<>
<div className="w-full flex items-center justify-between px-3 pt-2">
<div className="flex items-center gap-2">
{icon && icon}
<div className="flex flex-col">
<div className="font-semibold text-zinc-700 dark:text-zinc-300">
{title}
</div>
{subtitle && (
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{subtitle}
</div>
)}
</div>
</div>
{rightActions}
</>
</div>
) : isEntityList ? (
<div className="flex items-center justify-between w-full">
{title}