mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-30 19:06:23 +02:00
added ability to share workflows (#240)
This commit is contained in:
parent
ad7a0d313b
commit
1aaf5929f8
7 changed files with 283 additions and 35 deletions
|
|
@ -2,7 +2,7 @@
|
|||
import React from "react";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react";
|
||||
import { Button as CustomButton } from "@/components/ui/button";
|
||||
import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon } 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 { ProgressBar, ProgressStep } from "@/components/ui/progress-bar";
|
||||
|
||||
|
|
@ -35,6 +35,9 @@ interface TopBarProps {
|
|||
onStartTestTour?: () => void;
|
||||
onStartPublishTour?: () => void;
|
||||
onStartUseTour?: () => void;
|
||||
onShareWorkflow: () => void;
|
||||
shareUrl: string | null;
|
||||
onCopyShareUrl: () => void;
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
|
|
@ -66,6 +69,9 @@ export function TopBar({
|
|||
onStartTestTour,
|
||||
onStartPublishTour,
|
||||
onStartUseTour,
|
||||
onShareWorkflow,
|
||||
shareUrl,
|
||||
onCopyShareUrl,
|
||||
}: TopBarProps) {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -240,6 +246,27 @@ export function TopBar({
|
|||
<RadioIcon size={16} />
|
||||
Live workflow
|
||||
</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}
|
||||
|
|
@ -306,6 +333,27 @@ export function TopBar({
|
|||
<PenLine size={16} />
|
||||
Draft workflow
|
||||
</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}
|
||||
|
|
|
|||
|
|
@ -1389,15 +1389,12 @@ export function WorkflowEditor({
|
|||
onRevertModalClose();
|
||||
}
|
||||
|
||||
// Remove handleCopyJSON and add handleDownloadJSON
|
||||
function handleDownloadJSON() {
|
||||
// Helper: build exported JSON with masked prompt variables
|
||||
function buildWorkflowExportJson() {
|
||||
const workflow = state.present.workflow;
|
||||
|
||||
// Create a copy of the workflow and replace variable values with dummy text
|
||||
const workflowCopy = {
|
||||
...workflow,
|
||||
prompts: workflow.prompts.map(prompt => {
|
||||
// If this is a variable (base_prompt type), replace its value with dummy text
|
||||
if (prompt.type === 'base_prompt') {
|
||||
return {
|
||||
...prompt,
|
||||
|
|
@ -1407,8 +1404,12 @@ export function WorkflowEditor({
|
|||
return prompt;
|
||||
})
|
||||
};
|
||||
|
||||
const json = JSON.stringify(workflowCopy, null, 2);
|
||||
return JSON.stringify(workflowCopy, null, 2);
|
||||
}
|
||||
|
||||
// Download workflow as JSON file
|
||||
function handleDownloadJSON() {
|
||||
const json = buildWorkflowExportJson();
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
|
@ -1420,6 +1421,39 @@ export function WorkflowEditor({
|
|||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
// Share: upload JSON to server to get a share ID and reveal copy button
|
||||
const [shareUrl, setShareUrl] = useState<string | null>(null);
|
||||
async function handleShareWorkflow() {
|
||||
try {
|
||||
// POST to server to create a share token
|
||||
const json = buildWorkflowExportJson();
|
||||
const resp = await fetch('/api/shared-workflow', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: json,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.error('Failed to create share link');
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const createUrl = `${window.location.origin}/projects?shared=${encodeURIComponent(data.id)}`;
|
||||
setShareUrl(createUrl);
|
||||
} catch (e) {
|
||||
console.error('Error sharing workflow:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopyShareUrl() {
|
||||
if (!shareUrl) return;
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
setShowCopySuccess(true);
|
||||
setTimeout(() => setShowCopySuccess(false), 2000);
|
||||
}
|
||||
|
||||
// Cleanup blob URL on unmount
|
||||
// No-op cleanup; shareUrl is a normal URL now
|
||||
|
||||
const processQueue = useCallback(async (state: State, dispatch: React.Dispatch<Action>) => {
|
||||
if (saving.current || saveQueue.current.length === 0) return;
|
||||
|
||||
|
|
@ -1689,6 +1723,9 @@ export function WorkflowEditor({
|
|||
onUndo={() => dispatchGuarded({ type: "undo" })}
|
||||
onRedo={() => dispatchGuarded({ type: "redo" })}
|
||||
onDownloadJSON={handleDownloadJSON}
|
||||
onShareWorkflow={handleShareWorkflow}
|
||||
shareUrl={shareUrl}
|
||||
onCopyShareUrl={handleCopyShareUrl}
|
||||
onPublishWorkflow={handlePublishWorkflow}
|
||||
onChangeMode={onChangeMode}
|
||||
onRevertToLive={handleRevertToLive}
|
||||
|
|
|
|||
|
|
@ -156,23 +156,54 @@ export function BuildAssistantSection() {
|
|||
useEffect(() => {
|
||||
const urlPrompt = searchParams.get('prompt');
|
||||
const urlTemplate = searchParams.get('template');
|
||||
const sharedId = searchParams.get('shared');
|
||||
const importUrl = searchParams.get('importUrl');
|
||||
|
||||
if (urlPrompt || urlTemplate) {
|
||||
setAutoCreateLoading(true);
|
||||
createProjectWithOptions({
|
||||
template: urlTemplate || undefined,
|
||||
prompt: urlPrompt || undefined,
|
||||
router,
|
||||
onError: (error) => {
|
||||
console.error('Error auto-creating project:', error);
|
||||
setAutoCreateLoading(false);
|
||||
// Fall back to showing the form with the prompt pre-filled
|
||||
if (urlPrompt) {
|
||||
setUserPrompt(urlPrompt);
|
||||
const run = async () => {
|
||||
if (sharedId || importUrl) {
|
||||
try {
|
||||
setAutoCreateLoading(true);
|
||||
const qs = sharedId ? `id=${encodeURIComponent(sharedId)}` : `url=${encodeURIComponent(importUrl!)}`;
|
||||
const resp = await fetch(`/api/shared-workflow?${qs}`, { cache: 'no-store' });
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `Failed to load shared workflow (${resp.status})`);
|
||||
}
|
||||
const workflowObj = await resp.json();
|
||||
await createProjectFromJsonWithOptions({
|
||||
workflowJson: JSON.stringify(workflowObj),
|
||||
router,
|
||||
onError: (error) => {
|
||||
console.error('Error creating project from shared workflow:', error);
|
||||
setAutoCreateLoading(false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('Error auto-importing shared workflow:', err);
|
||||
setAutoCreateLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (urlPrompt || urlTemplate) {
|
||||
setAutoCreateLoading(true);
|
||||
createProjectWithOptions({
|
||||
template: urlTemplate || undefined,
|
||||
prompt: urlPrompt || undefined,
|
||||
router,
|
||||
onError: (error) => {
|
||||
console.error('Error auto-creating project:', error);
|
||||
setAutoCreateLoading(false);
|
||||
// Fall back to showing the form with the prompt pre-filled
|
||||
if (urlPrompt) {
|
||||
setUserPrompt(urlPrompt);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
}, [searchParams, router]);
|
||||
|
||||
const handleCreateAssistant = async () => {
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
|||
const searchParams = useSearchParams();
|
||||
const urlPrompt = searchParams.get('prompt');
|
||||
const urlTemplate = searchParams.get('template');
|
||||
const sharedId = searchParams.get('shared');
|
||||
const importUrl = searchParams.get('importUrl');
|
||||
|
||||
// Add this effect to update name when defaultName changes
|
||||
useEffect(() => {
|
||||
|
|
@ -165,29 +167,48 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
|||
// Add effect to handle URL parameters for auto-creation
|
||||
useEffect(() => {
|
||||
const handleAutoCreate = async () => {
|
||||
// Only auto-create if we have either a prompt or template, and we're not already loading
|
||||
if ((urlPrompt || urlTemplate) && !importLoading && !autoCreateLoading) {
|
||||
// Auto-create from template/prompt, or import from shared/id/url
|
||||
if ((urlPrompt || urlTemplate || sharedId || importUrl) && !importLoading && !autoCreateLoading) {
|
||||
setAutoCreateLoading(true);
|
||||
try {
|
||||
await createProjectWithOptions({
|
||||
template: urlTemplate || undefined,
|
||||
prompt: urlPrompt || undefined,
|
||||
router,
|
||||
onError: (error) => {
|
||||
// Auto-creation failed, show the form instead
|
||||
setBillingError(error instanceof Error ? error.message : String(error));
|
||||
setAutoCreateLoading(false);
|
||||
if (sharedId || importUrl) {
|
||||
// Fetch workflow JSON via our API route
|
||||
const qs = sharedId ? `id=${encodeURIComponent(sharedId)}` : `url=${encodeURIComponent(importUrl!)}`;
|
||||
const resp = await fetch(`/api/shared-workflow?${qs}`, { cache: 'no-store' });
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `Failed to load shared workflow (${resp.status})`);
|
||||
}
|
||||
});
|
||||
const workflowObj = await resp.json();
|
||||
await createProjectFromJsonWithOptions({
|
||||
workflowJson: JSON.stringify(workflowObj),
|
||||
router,
|
||||
onError: (error) => {
|
||||
setBillingError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await createProjectWithOptions({
|
||||
template: urlTemplate || undefined,
|
||||
prompt: urlPrompt || undefined,
|
||||
router,
|
||||
onError: (error) => {
|
||||
// Auto-creation failed, show the form instead
|
||||
setBillingError(error instanceof Error ? error.message : String(error));
|
||||
setAutoCreateLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error auto-creating project:', error);
|
||||
setBillingError(error instanceof Error ? error.message : String(error));
|
||||
setAutoCreateLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleAutoCreate();
|
||||
}, [urlPrompt, urlTemplate, importLoading, autoCreateLoading, router]);
|
||||
}, [urlPrompt, urlTemplate, sharedId, importUrl, importLoading, autoCreateLoading, router]);
|
||||
|
||||
// Inject glow animation styles
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue