Add auto-publish flow

This commit is contained in:
akhisud3195 2025-09-12 11:44:54 +04:00
parent b430c0cf15
commit 168b01ca70
3 changed files with 331 additions and 146 deletions

View file

@ -37,16 +37,32 @@ export function App({
const stored = window.localStorage.getItem(`workflow_mode_${initialProjectData.id}`); const stored = window.localStorage.getItem(`workflow_mode_${initialProjectData.id}`);
return stored === 'live' || stored === 'draft' ? stored : 'draft'; return stored === 'live' || stored === 'draft' ? stored : 'draft';
}); });
const [autoPublishEnabled, setAutoPublishEnabled] = useState(() => {
if (typeof window === 'undefined') return true; // Default to auto-publish
const stored = window.localStorage.getItem(`auto_publish_${initialProjectData.id}`);
return stored !== null ? stored === 'true' : true;
});
const [project, setProject] = useState<z.infer<typeof Project>>(initialProjectData); const [project, setProject] = useState<z.infer<typeof Project>>(initialProjectData);
const [dataSources, setDataSources] = useState<z.infer<typeof DataSource>[]>(initialDataSources); const [dataSources, setDataSources] = useState<z.infer<typeof DataSource>[]>(initialDataSources);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
console.log('workflow app.tsx render'); console.log('workflow app.tsx render');
const handleToggleAutoPublish = (enabled: boolean) => {
setAutoPublishEnabled(enabled);
if (typeof window !== 'undefined') {
window.localStorage.setItem(`auto_publish_${initialProjectData.id}`, enabled.toString());
}
};
// choose which workflow to display // choose which workflow to display
let workflow: z.infer<typeof Workflow> | undefined = project?.draftWorkflow; let workflow: z.infer<typeof Workflow> | undefined;
if (mode == 'live') { if (autoPublishEnabled) {
workflow = project?.liveWorkflow; // In auto-publish mode, always use draft (since they're synced)
workflow = project?.draftWorkflow;
} else {
// Manual mode: use current logic
workflow = mode === 'live' ? project?.liveWorkflow : project?.draftWorkflow;
} }
const reloadData = useCallback(async () => { const reloadData = useCallback(async () => {
@ -132,6 +148,8 @@ export function App({
{!loading && project && workflow && (dataSources !== null) && <WorkflowEditor {!loading && project && workflow && (dataSources !== null) && <WorkflowEditor
projectId={initialProjectData.id} projectId={initialProjectData.id}
isLive={mode == 'live'} isLive={mode == 'live'}
autoPublishEnabled={autoPublishEnabled}
onToggleAutoPublish={handleToggleAutoPublish}
workflow={workflow} workflow={workflow}
dataSources={dataSources} dataSources={dataSources}
projectConfig={project} projectConfig={project}

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input, ButtonGroup } from "@heroui/react"; import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input, ButtonGroup, Checkbox, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
import { Button as CustomButton } from "@/components/ui/button"; import { Button as CustomButton } from "@/components/ui/button";
import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon, ShareIcon } from "lucide-react"; import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon, ShareIcon } from "lucide-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@ -13,6 +13,8 @@ interface TopBarProps {
onProjectNameCommit: (value: string) => Promise<void>; onProjectNameCommit: (value: string) => Promise<void>;
publishing: boolean; publishing: boolean;
isLive: boolean; isLive: boolean;
autoPublishEnabled: boolean;
onToggleAutoPublish: (enabled: boolean) => void;
showCopySuccess: boolean; showCopySuccess: boolean;
showBuildModeBanner: boolean; showBuildModeBanner: boolean;
canUndo: boolean; canUndo: boolean;
@ -50,6 +52,8 @@ export function TopBar({
onProjectNameCommit, onProjectNameCommit,
publishing, publishing,
isLive, isLive,
autoPublishEnabled,
onToggleAutoPublish,
showCopySuccess, showCopySuccess,
showBuildModeBanner, showBuildModeBanner,
canUndo, canUndo,
@ -82,6 +86,15 @@ export function TopBar({
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const projectId = typeof (params as any).projectId === 'string' ? (params as any).projectId : (params as any).projectId?.[0]; const projectId = typeof (params as any).projectId === 'string' ? (params as any).projectId : (params as any).projectId?.[0];
// Share modal state
const { isOpen: isShareModalOpen, onOpen: onShareModalOpen, onClose: onShareModalClose } = useDisclosure();
const handleShareClick = () => {
onShareWorkflow(); // Call the original share function to generate URL
onShareModalOpen(); // Open the modal
};
// Progress bar steps with completion logic and current step detection // Progress bar steps with completion logic and current step detection
const step1Complete = hasAgentInstructionChanges; const step1Complete = hasAgentInstructionChanges;
const step2Complete = hasPlaygroundTested && hasAgentInstructionChanges; const step2Complete = hasPlaygroundTested && hasAgentInstructionChanges;
@ -99,9 +112,10 @@ export function TopBar({
]; ];
return ( 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="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="flex justify-between items-center">
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100"> <div className="workflow-version-selector flex items-center gap-3 -ml-1 pr-2 text-gray-800 dark:text-gray-100">
{/* Project Name Editor */} {/* Project Name Editor */}
<div className="flex flex-col min-w-0 max-w-xs"> <div className="flex flex-col min-w-0 max-w-xs">
<Input <Input
@ -121,15 +135,25 @@ export function TopBar({
size="sm" size="sm"
classNames={{ classNames={{
base: "max-w-xs", base: "max-w-xs",
input: "text-base font-semibold px-2", input: "text-sm font-semibold px-2",
inputWrapper: "min-h-[36px] h-[36px] 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>
{/* Show divider and mode indicator */} {/* Mode pill and auto-publish checkbox */}
{isLive && <div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>} <div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
{isLive ? (
{/* Mode pill */}
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 font-medium text-xs rounded-full">
<RadioIcon size={12} />
<span>
{autoPublishEnabled ? 'Live ' : (isLive ? 'Live ' : 'Draft')}
</span>
</div>
{/* Auto-publish checkbox or Switch to draft button */}
{!autoPublishEnabled && isLive ? (
<Button <Button
variant="solid" variant="solid"
size="sm" size="sm"
@ -140,10 +164,17 @@ export function TopBar({
Switch to draft Switch to draft
</Button> </Button>
) : ( ) : (
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 font-medium text-xs rounded-full"> !isLive && (
<PenLine size={12} /> <div className="flex items-center">
<span>Draft</span> <Checkbox
size="sm"
isSelected={autoPublishEnabled}
onValueChange={onToggleAutoPublish}
>
Auto-publish
</Checkbox>
</div> </div>
)
)} )}
</div> </div>
@ -274,9 +305,96 @@ export function TopBar({
})()} })()}
</div>)} </div>)}
{/* Deploy CTA - always visible */} {/* Deploy CTA - conditional based on auto-publish mode */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{isLive ? ( {autoPublishEnabled ? (
<>
{/* Auto-publish mode: Show Use Assistant button */}
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className="gap-2 px-3 h-8 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={14} />}
onPress={onUseAssistantClick}
>
Use Assistant
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Assistant access options">
<DropdownItem
key="chat"
startContent={<MessageCircleIcon size={16} />}
onPress={() => {
onUseAssistantClick();
onStartNewChatAndFocus();
}}
>
Chat with Assistant
</DropdownItem>
<DropdownItem
key="api-sdk"
startContent={<SettingsIcon size={16} />}
onPress={() => {
onUseAssistantClick();
if (projectId) { router.push(`/projects/${projectId}/config`); }
}}
>
API & SDK Settings
</DropdownItem>
<DropdownItem
key="manage-triggers"
startContent={<ZapIcon size={16} />}
onPress={() => {
onUseAssistantClick();
if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); }
}}
>
Manage Triggers
</DropdownItem>
</DropdownMenu>
</Dropdown>
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
<div className="flex">
<Button
variant="solid"
size="sm"
onPress={handleShareClick}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
startContent={<ShareIcon size={14} />}
>
Share
</Button>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
>
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Share actions">
<DropdownItem
key="download-json"
startContent={<DownloadIcon size={16} />}
onPress={onDownloadJSON}
>
Download JSON
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
</>
) : (
// Manual publish mode: Show current publish/live logic
isLive ? (
<> <>
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@ -327,44 +445,40 @@ export function TopBar({
<div className="flex items-center gap-2 ml-2"> <div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />} {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"> <div className="flex">
<RadioIcon size={16} /> <Button
Live workflow variant="solid"
size="sm"
onPress={handleShareClick}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
startContent={<ShareIcon size={14} />}
>
Share
</Button>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
>
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Share actions">
<DropdownItem
key="download-json"
startContent={<DownloadIcon size={16} />}
onPress={onDownloadJSON}
>
Download JSON
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div> </div>
<Tooltip content="Share Assistant">
<button
onClick={onShareWorkflow}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Share Assistant"
type="button"
>
<ShareIcon size={20} />
</button>
</Tooltip>
{shareUrl && (
<Tooltip content="Copy share URL">
<button
onClick={onCopyShareUrl}
className="px-2 py-1 text-xs bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:hover:bg-indigo-900/50 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700 rounded-md transition-colors"
type="button"
>
Copy URL
</button>
</Tooltip>
)}
<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 cursor-pointer"
aria-label="Download JSON"
type="button"
>
<DownloadIcon size={20} />
</button>
</Tooltip>
</div> </div>
</> </>) : (
) : ( // Draft mode in manual publish: Show publish button
<> <>
<div className="flex"> <div className="flex">
{(!hasAgents) ? ( {(!hasAgents) ? (
@ -375,7 +489,7 @@ export function TopBar({
size="sm" size="sm"
onPress={onPublishWorkflow} onPress={onPublishWorkflow}
isDisabled isDisabled
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed`} className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed min-w-[120px]`}
startContent={<RocketIcon size={14} />} startContent={<RocketIcon size={14} />}
data-tour-target="deploy" data-tour-target="deploy"
> >
@ -388,7 +502,7 @@ export function TopBar({
variant="solid" variant="solid"
size="sm" size="sm"
onPress={onPublishWorkflow} onPress={onPublishWorkflow}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-green-100 hover:bg-green-200 text-green-800 border-green-300`} className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-green-100 hover:bg-green-200 text-green-800 border-green-300 min-w-[132px]`}
startContent={<RocketIcon size={14} />} startContent={<RocketIcon size={14} />}
data-tour-target="deploy" data-tour-target="deploy"
> >
@ -442,52 +556,92 @@ export function TopBar({
<div className="flex items-center gap-2 ml-2"> <div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />} {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"> <div className="flex">
<RadioIcon size={16} /> <Button
Live workflow variant="solid"
</div>} size="sm"
{!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"> onPress={handleShareClick}
<PenLine size={16} /> className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
Draft workflow startContent={<ShareIcon size={14} />}
</div>}
<Tooltip content="Share Assistant">
<button
onClick={onShareWorkflow}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Share Assistant"
type="button"
> >
<ShareIcon size={20} /> Share
</button> </Button>
</Tooltip> <Dropdown>
{shareUrl && ( <DropdownTrigger>
<Tooltip content="Copy share URL"> <Button
<button variant="solid"
onClick={onCopyShareUrl} size="sm"
className="px-2 py-1 text-xs bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:hover:bg-indigo-900/50 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700 rounded-md transition-colors" className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
type="button"
> >
Copy URL <ChevronDownIcon size={12} />
</button> </Button>
</Tooltip> </DropdownTrigger>
)} <DropdownMenu aria-label="Share actions">
<Tooltip content="Download Assistant JSON"> <DropdownItem
<button key="download-json"
onClick={onDownloadJSON} startContent={<DownloadIcon size={16} />}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors cursor-pointer" onPress={onDownloadJSON}
aria-label="Download JSON"
type="button"
> >
<DownloadIcon size={20} /> Download JSON
</button> </DropdownItem>
</Tooltip> </DropdownMenu>
</Dropdown>
</div>
</div> </div>
</> </>
)
)} )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Share Modal */}
<Modal isOpen={isShareModalOpen} onClose={onShareModalClose} size="lg">
<ModalContent>
<ModalHeader className="flex flex-col gap-1">
Share Assistant
</ModalHeader>
<ModalBody>
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Share this assistant with others using the URL below:
</p>
{shareUrl ? (
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<input
type="text"
value={shareUrl || ''}
readOnly
className="flex-1 bg-transparent text-sm text-gray-700 dark:text-gray-300 outline-none"
/>
<Button
size="sm"
variant="solid"
onPress={onCopyShareUrl}
className="bg-indigo-100 hover:bg-indigo-200 text-indigo-800"
>
Copy
</Button>
</div>
) : (
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<Spinner size="sm" />
<span className="text-sm text-gray-600 dark:text-gray-400">
Generating share URL...
</span>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onShareModalClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
); );
} }

View file

@ -960,6 +960,8 @@ export function WorkflowEditor({
projectConfig, projectConfig,
eligibleModels, eligibleModels,
isLive, isLive,
autoPublishEnabled,
onToggleAutoPublish,
onChangeMode, onChangeMode,
onRevertToLive, onRevertToLive,
onProjectToolsUpdated, onProjectToolsUpdated,
@ -978,6 +980,8 @@ export function WorkflowEditor({
projectConfig: z.infer<typeof Project>; projectConfig: z.infer<typeof Project>;
eligibleModels: z.infer<typeof ModelsResponse> | "*"; eligibleModels: z.infer<typeof ModelsResponse> | "*";
isLive: boolean; isLive: boolean;
autoPublishEnabled: boolean;
onToggleAutoPublish: (enabled: boolean) => void;
onChangeMode: (mode: 'draft' | 'live') => void; onChangeMode: (mode: 'draft' | 'live') => void;
onRevertToLive: () => void; onRevertToLive: () => void;
onProjectToolsUpdated?: () => void; onProjectToolsUpdated?: () => void;
@ -1604,11 +1608,18 @@ export function WorkflowEditor({
saveQueue.current = []; saveQueue.current = [];
try { try {
if (autoPublishEnabled) {
// Auto-publish mode: save to both draft and live
await saveWorkflow(projectId, workflowToSave);
await publishWorkflow(projectId, workflowToSave);
} else {
// Manual mode: current logic
if (isLive) { if (isLive) {
return; return;
} else { } else {
await saveWorkflow(projectId, workflowToSave); await saveWorkflow(projectId, workflowToSave);
} }
}
} finally { } finally {
saving.current = false; saving.current = false;
if (saveQueue.current.length > 0) { if (saveQueue.current.length > 0) {
@ -1617,7 +1628,7 @@ export function WorkflowEditor({
dispatch({ type: "set_saving", saving: false }); dispatch({ type: "set_saving", saving: false });
} }
} }
}, [isLive, projectId]); }, [autoPublishEnabled, isLive, projectId]);
useEffect(() => { useEffect(() => {
if (state.present.pendingChanges && state.present.workflow) { if (state.present.pendingChanges && state.present.workflow) {
@ -1863,6 +1874,8 @@ export function WorkflowEditor({
onProjectNameCommit={handleProjectNameCommit} onProjectNameCommit={handleProjectNameCommit}
publishing={state.present.publishing} publishing={state.present.publishing}
isLive={isLive} isLive={isLive}
autoPublishEnabled={autoPublishEnabled}
onToggleAutoPublish={onToggleAutoPublish}
showCopySuccess={showCopySuccess} showCopySuccess={showCopySuccess}
showBuildModeBanner={showBuildModeBanner} showBuildModeBanner={showBuildModeBanner}
canUndo={state.currentIndex > 0} canUndo={state.currentIndex > 0}