mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
Change copilot and background panels to distinguish from playground (#204)
This commit is contained in:
parent
9304c1e5fd
commit
8488255d6d
9 changed files with 327 additions and 301 deletions
|
|
@ -7,11 +7,11 @@ import { CopilotMessage } from "../../../lib/types/copilot_types";
|
|||
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||
import { DataSource } from "@/app/lib/types/datasource_types";
|
||||
import { z } from "zod";
|
||||
import { Action as WorkflowDispatch } from "../workflow/workflow_editor";
|
||||
import { Action as WorkflowDispatch } from "@/app/projects/[projectId]/workflow/workflow_editor";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { ComposeBoxCopilot } from "@/components/common/compose-box-copilot";
|
||||
import { Messages } from "./components/messages";
|
||||
import { CopyIcon, CheckIcon, PlusIcon, XIcon, InfoIcon } from "lucide-react";
|
||||
import { CopyIcon, CheckIcon, PlusIcon, XIcon, InfoIcon, Sparkles } from "lucide-react";
|
||||
import { useCopilot } from "./use-copilot";
|
||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||
import { WithStringId } from "@/app/lib/types/types";
|
||||
|
|
@ -225,7 +225,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="shrink-0 px-1 pb-6">
|
||||
<div className="shrink-0 px-1">
|
||||
{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,16 +322,11 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
|
|||
variant="copilot"
|
||||
tourTarget="copilot"
|
||||
showWelcome={messages.length === 0}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-semibold text-zinc-700 dark:text-zinc-300">
|
||||
Skipper
|
||||
</div>
|
||||
<Tooltip content="A copilot to help you build and modify your workflow">
|
||||
<InfoIcon className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
icon={<Sparkles className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />}
|
||||
title="Skipper"
|
||||
subtitle="Build your assistant"
|
||||
rightActions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
|
|
@ -342,10 +337,6 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
|
|||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
||||
import { z } from "zod";
|
||||
import { Workflow} from "@/app/lib/types/workflow_types";
|
||||
import MarkdownContent from "@/app/lib/components/markdown-content";
|
||||
|
|
@ -153,16 +153,7 @@ function InternalAssistantMessage({ content }: { content: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
type ActionPanelBlock = {
|
||||
part: {
|
||||
type: 'action';
|
||||
action: any;
|
||||
} | {
|
||||
type: 'streaming_action';
|
||||
action: any;
|
||||
};
|
||||
actionIndex: number;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* AssistantMessage component that renders copilot responses with action cards.
|
||||
|
|
@ -194,26 +185,24 @@ function AssistantMessage({
|
|||
// Remove autoApplyEnabled and useEffect for auto-apply
|
||||
|
||||
// parse actions from parts
|
||||
let parsed: z.infer<typeof CopilotResponsePart>[] = [];
|
||||
for (const block of blocks) {
|
||||
if (block.type === 'text') {
|
||||
parsed.push({
|
||||
type: 'text',
|
||||
content: block.content,
|
||||
});
|
||||
} else {
|
||||
parsed.push(enrich(block.content));
|
||||
const parsed = useMemo(() => {
|
||||
const result: z.infer<typeof CopilotResponsePart>[] = [];
|
||||
for (const block of blocks) {
|
||||
if (block.type === 'text') {
|
||||
result.push({
|
||||
type: 'text',
|
||||
content: block.content,
|
||||
});
|
||||
} else {
|
||||
result.push(enrich(block.content));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [blocks]);
|
||||
|
||||
// Only render text outside the panel
|
||||
const textBlocks = parsed.filter(part => part.type === 'text');
|
||||
// All cards (action and streaming_action) go inside the panel
|
||||
const cardBlocks: ActionPanelBlock[] = parsed
|
||||
.map((part, actionIndex) => ({ part, actionIndex }))
|
||||
.filter(({ part }) => part.type === 'action' || part.type === 'streaming_action') as ActionPanelBlock[];
|
||||
const hasCards = cardBlocks.length > 0;
|
||||
const totalActions = cardBlocks.filter(({ part }) => part.type === 'action').length;
|
||||
// Count action cards for tracking
|
||||
const actionParts = parsed.filter(part => part.type === 'action' || part.type === 'streaming_action');
|
||||
const totalActions = parsed.filter(part => part.type === 'action').length;
|
||||
const appliedCount = Array.from(appliedActions).length;
|
||||
const pendingCount = Math.max(0, totalActions - appliedCount);
|
||||
const allApplied = pendingCount === 0 && totalActions > 0;
|
||||
|
|
@ -307,9 +296,14 @@ function AssistantMessage({
|
|||
// Memoized handleApplyAll for useEffect dependencies
|
||||
const handleApplyAll = useCallback(() => {
|
||||
// Find all unapplied action indices
|
||||
const unapplied = cardBlocks
|
||||
const unapplied = parsed
|
||||
.map((part, idx) => ({ part, actionIndex: idx }))
|
||||
.filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex))
|
||||
.map(({ part, actionIndex }) => ({ action: part.action, actionIndex }));
|
||||
.map(({ part, actionIndex }) => ({
|
||||
action: part.type === 'action' ? part.action : null,
|
||||
actionIndex
|
||||
}))
|
||||
.filter(({ action }) => action !== null);
|
||||
|
||||
// Synchronously apply all unapplied actions
|
||||
unapplied.forEach(({ action, actionIndex }) => {
|
||||
|
|
@ -322,7 +316,7 @@ function AssistantMessage({
|
|||
unapplied.forEach(({ actionIndex }) => next.add(actionIndex));
|
||||
return next;
|
||||
});
|
||||
}, [cardBlocks, appliedActions, setAppliedActions, applyAction]);
|
||||
}, [parsed, appliedActions, setAppliedActions, applyAction]);
|
||||
|
||||
// Manual single apply (from card)
|
||||
const handleSingleApply = (action: any, actionIndex: number) => {
|
||||
|
|
@ -343,37 +337,20 @@ function AssistantMessage({
|
|||
// Removed useEffect for auto-apply
|
||||
|
||||
// Find streaming/ongoing card and extract name
|
||||
const streamingBlock = cardBlocks.find(({ part }) => part.type === 'streaming_action');
|
||||
const streamingPart = parsed.find(part => part.type === 'streaming_action');
|
||||
let streamingLine = '';
|
||||
if (streamingBlock && streamingBlock.part.type === 'streaming_action' && streamingBlock.part.action && streamingBlock.part.action.name) {
|
||||
streamingLine = `Generating ${streamingBlock.part.action.name}...`;
|
||||
if (streamingPart && streamingPart.type === 'streaming_action' && streamingPart.action && streamingPart.action.name) {
|
||||
streamingLine = `Generating ${streamingPart.action.name}...`;
|
||||
}
|
||||
|
||||
// Find the first card index
|
||||
const firstCardIdx = parsed.findIndex(part => part.type === 'action' || part.type === 'streaming_action');
|
||||
// Group blocks into: beforePanel, cardBlocks, afterPanel
|
||||
const beforePanel = firstCardIdx === -1 ? parsed : parsed.slice(0, firstCardIdx);
|
||||
const panelBlocks = firstCardIdx === -1 ? [] : parsed.slice(firstCardIdx).filter(part => part.type === 'action' || part.type === 'streaming_action');
|
||||
// Find where the card blocks end (first non-card after first card)
|
||||
let afterPanelStart = firstCardIdx;
|
||||
if (firstCardIdx !== -1) {
|
||||
for (let i = firstCardIdx; i < parsed.length; i++) {
|
||||
if (parsed[i].type !== 'action' && parsed[i].type !== 'streaming_action') {
|
||||
afterPanelStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const afterPanel = (firstCardIdx !== -1 && afterPanelStart > firstCardIdx) ? parsed.slice(afterPanelStart) : [];
|
||||
|
||||
// Only show Apply All button if all cards are loaded (no streaming_action cards) and streaming is finished
|
||||
const allCardsLoaded = !loading && panelBlocks.length > 0 && panelBlocks.every(part => part.type === 'action');
|
||||
const allCardsLoaded = !loading && actionParts.length > 0 && actionParts.every(part => part.type === 'action');
|
||||
// When all cards are loaded, show summary of agents created/updated
|
||||
let completedSummary = '';
|
||||
if (allCardsLoaded && totalActions > 0) {
|
||||
// Count how many are create vs edit
|
||||
const createCount = cardBlocks.filter(({ part }) => part.type === 'action' && part.action.action === 'create_new').length;
|
||||
const editCount = cardBlocks.filter(({ part }) => part.type === 'action' && part.action.action === 'edit').length;
|
||||
const createCount = parsed.filter(part => part.type === 'action' && part.action.action === 'create_new').length;
|
||||
const editCount = parsed.filter(part => part.type === 'action' && part.action.action === 'edit').length;
|
||||
const parts = [];
|
||||
if (createCount > 0) parts.push(`${createCount} agent${createCount > 1 ? 's' : ''} created`);
|
||||
if (editCount > 0) parts.push(`${editCount} agent${editCount > 1 ? 's' : ''} updated`);
|
||||
|
|
@ -381,51 +358,13 @@ function AssistantMessage({
|
|||
}
|
||||
|
||||
// Detect if any card has an error or is cancelled
|
||||
const hasPanelWarning = cardBlocks.some(
|
||||
({ part }) =>
|
||||
const hasPanelWarning = parsed.some(
|
||||
part =>
|
||||
part.type === 'action' &&
|
||||
part.action &&
|
||||
(part.action.error || ('cancelled' in part.action && part.action.cancelled))
|
||||
);
|
||||
|
||||
// Ticker summary for collapsed state (two lines)
|
||||
const ticker = (
|
||||
<div className="flex flex-col">
|
||||
{allCardsLoaded && completedSummary ? (
|
||||
<span className="font-medium text-xs sm:text-sm">{completedSummary}</span>
|
||||
) : streamingLine && (
|
||||
<span className="font-medium text-xs sm:text-sm">{streamingLine}</span>
|
||||
)}
|
||||
<span className="font-medium text-xs sm:text-sm">{appliedCount} applied, {pendingCount} pending</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const applyAllButton = (
|
||||
<button
|
||||
onClick={handleApplyAll}
|
||||
disabled={allApplied} // Changed to allApplied
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-full font-medium text-sm transition-colors duration-200
|
||||
${
|
||||
allApplied
|
||||
? 'bg-zinc-100 dark:bg-zinc-800 text-zinc-400 cursor-not-allowed border border-zinc-200 dark:border-zinc-700 shadow-none'
|
||||
: 'bg-blue-100 dark:bg-zinc-900 text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-zinc-800 border border-blue-200 dark:border-zinc-800 shadow-sm'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{allApplied ? (
|
||||
<>
|
||||
<CheckCheckIcon size={16} />
|
||||
All applied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCheckIcon size={16} />
|
||||
Apply all
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
// Utility to filter out divider/empty markdown blocks
|
||||
function isNonDividerMarkdown(content: string) {
|
||||
const trimmed = content.trim();
|
||||
|
|
@ -435,9 +374,6 @@ function AssistantMessage({
|
|||
);
|
||||
}
|
||||
|
||||
// Restore panelOpen state if missing
|
||||
const [panelOpen, setPanelOpen] = useState(false); // collapsed by default
|
||||
|
||||
// At the end of the render, call onStatusBarChange with the current status bar props
|
||||
// Track the latest status bar info
|
||||
const latestStatusBar = useRef<any>(null);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Chat } from "./components/chat";
|
|||
import { Panel } from "@/components/common/panel-common";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip } from "@heroui/react";
|
||||
import { CheckIcon, CopyIcon, PlusIcon, InfoIcon, BugIcon, BugOffIcon } from "lucide-react";
|
||||
import { CheckIcon, CopyIcon, PlusIcon, InfoIcon, BugIcon, BugOffIcon, MessageCircle } from "lucide-react";
|
||||
|
||||
export function App({
|
||||
hidden = false,
|
||||
|
|
@ -56,16 +56,11 @@ export function App({
|
|||
className={`${hidden ? 'hidden' : 'block'}`}
|
||||
variant="playground"
|
||||
tourTarget="playground"
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-semibold text-zinc-700 dark:text-zinc-300">
|
||||
Playground
|
||||
</div>
|
||||
<Tooltip content="Test your workflow and chat with your agents in real-time">
|
||||
<InfoIcon className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
icon={<MessageCircle className="w-5 h-5 text-blue-600 dark:text-blue-400" />}
|
||||
title="Playground"
|
||||
subtitle="Chat with your assistant"
|
||||
rightActions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
|
|
@ -90,10 +85,6 @@ export function App({
|
|||
<BugOffIcon className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react";
|
||||
import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon } from "lucide-react";
|
||||
|
||||
interface TopBarProps {
|
||||
localProjectName: string;
|
||||
projectNameError: string | null;
|
||||
onProjectNameChange: (value: string) => void;
|
||||
publishing: boolean;
|
||||
isLive: boolean;
|
||||
showCopySuccess: boolean;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
showCopilot: boolean;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onDownloadJSON: () => void;
|
||||
onPublishWorkflow: () => void;
|
||||
onChangeMode: (mode: 'draft' | 'live') => void;
|
||||
onRevertToLive: () => void;
|
||||
onToggleCopilot: () => void;
|
||||
onSettingsModalOpen: () => void;
|
||||
onTriggersModalOpen: () => void;
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
localProjectName,
|
||||
projectNameError,
|
||||
onProjectNameChange,
|
||||
publishing,
|
||||
isLive,
|
||||
showCopySuccess,
|
||||
canUndo,
|
||||
canRedo,
|
||||
showCopilot,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onDownloadJSON,
|
||||
onPublishWorkflow,
|
||||
onChangeMode,
|
||||
onRevertToLive,
|
||||
onToggleCopilot,
|
||||
onSettingsModalOpen,
|
||||
onTriggersModalOpen,
|
||||
}: TopBarProps) {
|
||||
return (
|
||||
<div className="rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm border border-zinc-200 dark:border-zinc-800 px-5 py-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
|
||||
{/* Project Name Editor */}
|
||||
<div className="flex flex-col min-w-0 max-w-xs">
|
||||
<Input
|
||||
type="text"
|
||||
value={localProjectName}
|
||||
onChange={(e) => onProjectNameChange(e.target.value)}
|
||||
isInvalid={!!projectNameError}
|
||||
errorMessage={projectNameError}
|
||||
placeholder="Project name..."
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
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"
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</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">
|
||||
<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
|
||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||
title="Undo"
|
||||
disabled={!canUndo}
|
||||
onClick={onUndo}
|
||||
>
|
||||
<UndoIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||
title="Redo"
|
||||
disabled={!canRedo}
|
||||
onClick={onRedo}
|
||||
>
|
||||
<RedoIcon size={16} />
|
||||
</button>
|
||||
</>}
|
||||
|
||||
{/* Deploy CTA - always visible */}
|
||||
<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"
|
||||
startContent={<RocketIcon size={16} />}
|
||||
data-tour-target="deploy"
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<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"
|
||||
>
|
||||
<ChevronDownIcon size={14} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Deploy actions">
|
||||
<DropdownItem
|
||||
key="settings"
|
||||
startContent={<SettingsIcon size={16} />}
|
||||
onPress={onSettingsModalOpen}
|
||||
>
|
||||
API & SDK settings
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="manage-triggers"
|
||||
startContent={<ZapIcon size={16} />}
|
||||
onPress={onTriggersModalOpen}
|
||||
>
|
||||
Manage triggers
|
||||
</DropdownItem>
|
||||
{!isLive ? (
|
||||
<>
|
||||
<DropdownItem
|
||||
key="view-live"
|
||||
startContent={<RadioIcon size={16} />}
|
||||
onPress={() => onChangeMode('live')}
|
||||
>
|
||||
View live version
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="reset-to-live"
|
||||
startContent={<AlertTriangle size={16} />}
|
||||
onPress={onRevertToLive}
|
||||
className="text-red-600 dark:text-red-400"
|
||||
>
|
||||
Reset to live version
|
||||
</DropdownItem>
|
||||
</>
|
||||
) : null}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ import { InputField } from "@/app/lib/components/input-field";
|
|||
import { VoiceSection } from "../config/components/voice";
|
||||
import { ChatWidgetSection } from "../config/components/project";
|
||||
import { TriggersModal } from "./components/TriggersModal";
|
||||
import { TopBar } from "./components/TopBar";
|
||||
|
||||
enablePatches();
|
||||
|
||||
|
|
@ -1221,161 +1222,31 @@ export function WorkflowEditor({
|
|||
onSelectTool: handleSelectTool,
|
||||
onSelectPrompt: handleSelectPrompt,
|
||||
}}>
|
||||
<div className="flex flex-col h-full relative">
|
||||
<div className="shrink-0 flex justify-between items-center pb-6">
|
||||
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
|
||||
{/* Project Name Editor */}
|
||||
<div className="flex flex-col min-w-0 max-w-xs">
|
||||
<InputField
|
||||
type="text"
|
||||
value={localProjectName}
|
||||
onChange={handleProjectNameChange}
|
||||
error={projectNameError}
|
||||
placeholder="Project name..."
|
||||
className="text-lg font-semibold !min-h-[24px] !py-0.5 !border !border-gray-200 dark:!border-gray-700 !rounded-lg"
|
||||
inline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600"></div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{state.present.publishing && <Spinner size="sm" />}
|
||||
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
|
||||
<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-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
|
||||
<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={handleDownloadJSON}
|
||||
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>
|
||||
{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">
|
||||
<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
|
||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||
title="Undo"
|
||||
disabled={state.currentIndex <= 0}
|
||||
onClick={() => dispatch({ type: "undo" })}
|
||||
>
|
||||
<UndoIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
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>
|
||||
</>}
|
||||
|
||||
{/* Deploy CTA - always visible */}
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onPress={handlePublishWorkflow}
|
||||
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm rounded-r-none"
|
||||
startContent={<RocketIcon size={16} />}
|
||||
data-tour-target="deploy"
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<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"
|
||||
>
|
||||
<ChevronDownIcon size={14} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Deploy actions">
|
||||
<DropdownItem
|
||||
key="settings"
|
||||
startContent={<SettingsIcon size={16} />}
|
||||
onPress={onSettingsModalOpen}
|
||||
>
|
||||
API & SDK settings
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="manage-triggers"
|
||||
startContent={<ZapIcon size={16} />}
|
||||
onPress={onTriggersModalOpen}
|
||||
>
|
||||
Manage triggers
|
||||
</DropdownItem>
|
||||
{!isLive ? (
|
||||
<>
|
||||
<DropdownItem
|
||||
key="view-live"
|
||||
startContent={<RadioIcon size={16} />}
|
||||
onPress={() => onChangeMode('live')}
|
||||
>
|
||||
View live version
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="reset-to-live"
|
||||
startContent={<AlertTriangle size={16} />}
|
||||
onPress={handleRevertToLive}
|
||||
className="text-red-600 dark:text-red-400"
|
||||
>
|
||||
Reset to live version
|
||||
</DropdownItem>
|
||||
</>
|
||||
) : null}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</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"
|
||||
>
|
||||
Switch to draft
|
||||
</Button>
|
||||
</div>}
|
||||
|
||||
{!isLive && <Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onPress={() => setShowCopilot(!showCopilot)}
|
||||
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
|
||||
startContent={showCopilot ? null : <Sparkles size={16} />}
|
||||
>
|
||||
{showCopilot ? "Hide Skipper" : "Skipper"}
|
||||
</Button>}
|
||||
</div>
|
||||
</div>
|
||||
<ResizablePanelGroup direction="horizontal" className="grow flex overflow-auto gap-1">
|
||||
<div className="h-full flex flex-col gap-5">
|
||||
{/* Top Bar - Isolated like sidebar */}
|
||||
<TopBar
|
||||
localProjectName={localProjectName}
|
||||
projectNameError={projectNameError}
|
||||
onProjectNameChange={handleProjectNameChange}
|
||||
publishing={state.present.publishing}
|
||||
isLive={isLive}
|
||||
showCopySuccess={showCopySuccess}
|
||||
canUndo={state.currentIndex > 0}
|
||||
canRedo={state.currentIndex < state.patches.length}
|
||||
showCopilot={showCopilot}
|
||||
onUndo={() => dispatch({ type: "undo" })}
|
||||
onRedo={() => dispatch({ type: "redo" })}
|
||||
onDownloadJSON={handleDownloadJSON}
|
||||
onPublishWorkflow={handlePublishWorkflow}
|
||||
onChangeMode={onChangeMode}
|
||||
onRevertToLive={handleRevertToLive}
|
||||
onToggleCopilot={() => setShowCopilot(!showCopilot)}
|
||||
onSettingsModalOpen={onSettingsModalOpen}
|
||||
onTriggersModalOpen={onTriggersModalOpen}
|
||||
/>
|
||||
|
||||
{/* Content Area */}
|
||||
<ResizablePanelGroup direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
|
||||
<ResizablePanel minSize={10} defaultSize={PANEL_RATIOS.entityList}>
|
||||
<div className="flex flex-col h-full">
|
||||
<EntityList
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default function AppLayout({ children, useAuth = false, useBilling = fals
|
|||
return (
|
||||
<div className="h-screen flex gap-5 p-5 bg-zinc-50 dark:bg-zinc-900">
|
||||
{/* Sidebar with improved shadow and blur */}
|
||||
<div className="overflow-hidden rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm">
|
||||
<div className="h-full overflow-hidden rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm">
|
||||
<Sidebar
|
||||
projectId={projectId ?? undefined}
|
||||
useAuth={useAuth}
|
||||
|
|
@ -53,8 +53,8 @@ export default function AppLayout({ children, useAuth = false, useBilling = fals
|
|||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<main className="flex gap-2 flex-col flex-1 overflow-auto rounded-xl bg-white dark:bg-zinc-800 shadow-sm p-4">
|
||||
{billingPastDue && <div className="shrink-0">
|
||||
<main className="flex-1 h-full overflow-auto">
|
||||
{billingPastDue && <div className="shrink-0 mb-2">
|
||||
<div className="bg-red-50 text-red-500 px-2 py-1 text-sm rounded-md flex items-center gap-2">
|
||||
<span>Your subscription is past due. Please update your payment information to avoid losing access to your projects.</span>
|
||||
<Button
|
||||
|
|
@ -68,9 +68,7 @@ export default function AppLayout({ children, useAuth = false, useBilling = fals
|
|||
</Button>
|
||||
</div>
|
||||
</div>}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue