feat: enable workflows to be embedded in websites as a script tag (#47)

* feat: add deployment configuration options

* Simplify EmbedDialog

* Add options for inline vs floating embedding of agent
This commit is contained in:
Abhishek 2025-11-15 17:32:37 +05:30 committed by GitHub
parent 5e4aef346d
commit 99a768f291
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3551 additions and 645 deletions

View file

@ -7,7 +7,7 @@ import {
Panel,
ReactFlow,
} from "@xyflow/react";
import { BrushCleaning, Maximize2, Minus, Plus, Settings, Variable } from 'lucide-react';
import { BrushCleaning, Maximize2, Minus, Plus, Rocket, Settings, Variable } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import WorkflowLayout from '@/app/workflow/WorkflowLayout';
@ -20,6 +20,7 @@ import AddNodePanel from "../../../components/flow/AddNodePanel";
import CustomEdge from "../../../components/flow/edges/CustomEdge";
import { AgentNode, EndCall, GlobalNode, StartCall } from "../../../components/flow/nodes";
import { ConfigurationsDialog } from './components/ConfigurationsDialog';
import { EmbedDialog } from './components/EmbedDialog';
import { TemplateContextVariablesDialog } from './components/TemplateContextVariablesDialog';
import WorkflowHeader from "./components/WorkflowHeader";
import { WorkflowTabs } from './components/WorkflowTabs';
@ -76,6 +77,7 @@ interface RenderWorkflowProps {
function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialTemplateContextVariables, initialWorkflowConfigurations, user, getAccessToken }: RenderWorkflowProps) {
const [isContextVarsDialogOpen, setIsContextVarsDialogOpen] = useState(false);
const [isConfigurationsDialogOpen, setIsConfigurationsDialogOpen] = useState(false);
const [isEmbedDialogOpen, setIsEmbedDialogOpen] = useState(false);
const {
rfInstance,
@ -218,6 +220,22 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
<p>Template Context Variables</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => setIsEmbedDialogOpen(true)}
className="bg-white shadow-sm hover:shadow-md"
>
<Rocket className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>Deploy Workflow</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</Panel>
@ -317,6 +335,14 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
templateContextVariables={templateContextVariables}
onSave={saveTemplateContextVariables}
/>
<EmbedDialog
open={isEmbedDialogOpen}
onOpenChange={setIsEmbedDialogOpen}
workflowId={workflowId}
workflowName={workflowName}
getAccessToken={getAccessToken}
/>
</WorkflowLayout>
</WorkflowProvider>
);

View file

@ -0,0 +1,507 @@
import { Check, Copy, Loader2, Plus, Rocket, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { client } from "@/client/client.gen";
import {
createOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPost,
deactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDelete,
getEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGet,
} from "@/client/sdk.gen";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
interface EmbedDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workflowId: number;
workflowName: string;
getAccessToken: () => Promise<string>;
}
interface EmbedToken {
id: number;
token: string;
allowed_domains: string[] | null;
settings: Record<string, unknown> | null;
is_active: boolean;
usage_count: number;
usage_limit: number | null;
expires_at: string | null;
created_at: string;
embed_script: string;
}
export function EmbedDialog({
open,
onOpenChange,
workflowId,
workflowName,
getAccessToken,
}: EmbedDialogProps) {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [embedToken, setEmbedToken] = useState<EmbedToken | null>(null);
const [copied, setCopied] = useState(false);
// Form state
const [isEnabled, setIsEnabled] = useState(false);
const [domains, setDomains] = useState<string[]>([]);
const [newDomain, setNewDomain] = useState("");
const [embedMode, setEmbedMode] = useState<"floating" | "inline">("floating");
const [position, setPosition] = useState("bottom-right");
const [buttonText, setButtonText] = useState("Start Voice Call");
const [buttonColor, setButtonColor] = useState("#3B82F6");
const loadEmbedToken = useCallback(async () => {
setLoading(true);
try {
const token = await getAccessToken();
client.setConfig({
baseUrl: window.location.origin.replace(/:\d+$/, ':8000'),
headers: { Authorization: `Bearer ${token}` },
});
const response = await getEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGet({
path: { workflow_id: workflowId },
});
if (response.data) {
setEmbedToken(response.data as EmbedToken);
setIsEnabled(response.data.is_active);
// Load settings
if (response.data.settings) {
const settings = response.data.settings as Record<string, string>;
setEmbedMode((settings.embedMode as "floating" | "inline") || "floating");
setPosition(settings.position || "bottom-right");
setButtonText(settings.buttonText || "Start Voice Call");
setButtonColor(settings.buttonColor || "#3B82F6");
}
// Load domains
if (response.data.allowed_domains) {
setDomains(response.data.allowed_domains);
}
}
} catch (error) {
console.error("Failed to load embed token:", error);
} finally {
setLoading(false);
}
}, [workflowId, getAccessToken]);
useEffect(() => {
if (open) {
loadEmbedToken();
}
}, [open, loadEmbedToken]);
const handleSave = async () => {
setSaving(true);
try {
const token = await getAccessToken();
client.setConfig({
baseUrl: window.location.origin.replace(/:\d+$/, ':8000'),
headers: { Authorization: `Bearer ${token}` },
});
if (!isEnabled && embedToken) {
// Deactivate token
await deactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDelete({
path: { workflow_id: workflowId },
});
setEmbedToken(null);
} else if (isEnabled) {
// Create or update token
const response = await createOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPost({
path: { workflow_id: workflowId },
body: {
allowed_domains: domains.length > 0 ? domains : null,
settings: {
embedMode,
position,
buttonText,
buttonColor,
size: "medium",
autoStart: false,
containerId: embedMode === "inline" ? "dograh-inline-container" : undefined,
},
usage_limit: null,
expires_in_days: null,
},
});
if (response.data) {
setEmbedToken(response.data as EmbedToken);
}
}
// Don't close modal after saving - let user copy the embed code
} catch (error) {
console.error("Failed to save embed token:", error);
} finally {
setSaving(false);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const addDomain = () => {
if (newDomain.trim() && !domains.includes(newDomain.trim())) {
setDomains([...domains, newDomain.trim()]);
setNewDomain("");
}
};
const removeDomain = (domain: string) => {
setDomains(domains.filter(d => d !== domain));
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
addDomain();
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" />
Deploy Workflow
</DialogTitle>
<DialogDescription>
Embed &quot;{workflowName}&quot; on any website with a simple script tag
</DialogDescription>
</DialogHeader>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-gray-500" />
</div>
) : (
<div className="space-y-6">
{/* Enable/Disable Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="embed-enabled">Enable Embedding</Label>
<p className="text-sm text-muted-foreground">
Allow this workflow to be embedded on external websites
</p>
</div>
<Switch
id="embed-enabled"
checked={isEnabled}
onCheckedChange={setIsEnabled}
/>
</div>
{isEnabled && (
<>
<Separator />
{/* Allowed Domains */}
<div className="space-y-3">
<Label>
Allowed Domains
<span className="text-xs text-muted-foreground ml-2">
(leave empty to allow all domains)
</span>
</Label>
{/* Domain Input */}
<div className="flex gap-2">
<Input
placeholder="example.com or *.example.com"
value={newDomain}
onChange={(e) => setNewDomain(e.target.value)}
onKeyPress={handleKeyPress}
/>
<Button
type="button"
size="icon"
variant="outline"
onClick={addDomain}
disabled={!newDomain.trim()}
>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* Domain List */}
{domains.length > 0 && (
<div className="space-y-2">
{domains.map((domain, index) => (
<div
key={index}
className="flex items-center justify-between bg-muted/50 rounded-lg px-3 py-2"
>
<span className="text-sm font-mono">{domain}</span>
<Button
type="button"
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={() => removeDomain(domain)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
</div>
{/* Embed Mode Selection */}
<div className="space-y-4">
<Label>Embed Mode</Label>
<div className="grid grid-cols-2 gap-4">
<button
type="button"
onClick={() => setEmbedMode("floating")}
className={`p-4 rounded-lg border-2 transition-all ${
embedMode === "floating"
? "border-primary bg-primary/5"
: "border-muted hover:border-muted-foreground/20"
}`}
>
<div className="space-y-2">
<div className="font-medium">Floating Widget</div>
<div className="text-xs text-muted-foreground">
Shows as a button in corner of the page
</div>
</div>
</button>
<button
type="button"
onClick={() => setEmbedMode("inline")}
className={`p-4 rounded-lg border-2 transition-all ${
embedMode === "inline"
? "border-primary bg-primary/5"
: "border-muted hover:border-muted-foreground/20"
}`}
>
<div className="space-y-2">
<div className="font-medium">Inline Component</div>
<div className="text-xs text-muted-foreground">
Embeds directly in your page content
</div>
</div>
</button>
</div>
</div>
{/* Configuration based on mode */}
<div className="space-y-4">
<Label>Configuration</Label>
{embedMode === "floating" ? (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="position" className="text-sm">Position</Label>
<Select value={position} onValueChange={setPosition}>
<SelectTrigger id="position">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bottom-right">Bottom Right</SelectItem>
<SelectItem value="bottom-left">Bottom Left</SelectItem>
<SelectItem value="top-right">Top Right</SelectItem>
<SelectItem value="top-left">Top Left</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="button-color" className="text-sm">Button Color</Label>
<div className="flex gap-2">
<Input
id="button-color-picker"
type="color"
value={buttonColor}
onChange={(e) => setButtonColor(e.target.value)}
className="w-14 h-10 cursor-pointer"
/>
<Input
id="button-color"
value={buttonColor}
onChange={(e) => setButtonColor(e.target.value)}
placeholder="#3B82F6"
className="flex-1"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="button-text" className="text-sm">Button Text</Label>
<Input
id="button-text"
value={buttonText}
onChange={(e) => setButtonText(e.target.value)}
placeholder="Start Voice Call"
/>
</div>
</>
) : (
<>
<div className="space-y-3">
<div className="rounded-lg bg-muted/50 p-4">
<h4 className="font-medium mb-2">Integration Instructions</h4>
<ul className="text-sm space-y-2 text-muted-foreground">
<li> Add a div with id=&quot;dograh-inline-container&quot; where you want the widget</li>
<li> The widget will render inside this container</li>
<li> You have full control over the container&apos;s styling</li>
<li> Call window.DograhWidget.start() to begin the call</li>
<li> Call window.DograhWidget.end() to end the call</li>
</ul>
</div>
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-4 border border-blue-200 dark:border-blue-800">
<h4 className="font-medium mb-2 text-blue-900 dark:text-blue-100">Example React Component</h4>
<pre className="text-xs overflow-x-auto">
<code className="text-blue-800 dark:text-blue-200">{`export function DograhAgent() {
const [isCallActive, setIsCallActive] = useState(false);
useEffect(() => {
// Widget will auto-initialize when script loads
window.DograhWidget?.onCallStart(() => {
setIsCallActive(true);
});
window.DograhWidget?.onCallEnd(() => {
setIsCallActive(false);
});
}, []);
return (
<div className="my-8">
<h2>Talk to Our Agent</h2>
<div id="dograh-inline-container" className="min-h-[400px]">
{/* Widget renders here */}
</div>
<button
onClick={() => window.DograhWidget?.start()}
disabled={isCallActive}
>
Start Call
</button>
</div>
);
}`}</code>
</pre>
</div>
</div>
</>
)}
{/* Preview for floating mode only */}
{embedMode === "floating" && (
<div className="rounded-lg border bg-background p-4 flex items-center justify-center">
<button
className="px-5 py-2.5 rounded-full font-medium shadow-lg hover:shadow-xl transition-all flex items-center gap-2"
style={{
backgroundColor: buttonColor,
color: "white",
}}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
</svg>
{buttonText}
</button>
</div>
)}
</div>
<Separator />
{/* Save Button */}
<div className="flex justify-end">
<Button onClick={handleSave} disabled={saving}>
{saving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Saving...
</>
) : (
"Save Configurations"
)}
</Button>
</div>
{/* Embed Script (shows after saving) */}
{embedToken && embedToken.is_active && (
<>
<Separator />
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Embed Code</Label>
<Button
size="sm"
variant="outline"
onClick={() => copyToClipboard(embedToken.embed_script)}
>
{copied ? (
<>
<Check className="h-4 w-4 mr-1" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
Copy Code
</>
)}
</Button>
</div>
<div className="relative">
<pre className="bg-muted/50 rounded-lg p-4 text-xs overflow-x-auto whitespace-pre-wrap break-all">
<code>{embedToken.embed_script}</code>
</pre>
</div>
<p className="text-xs text-muted-foreground">
Add this script to your website&apos;s HTML to enable the voice widget.
Configuration changes will apply automatically without re-embedding.
</p>
</div>
</>
)}
</>
)}
</div>
)}
</DialogContent>
</Dialog>
);
}

View file

@ -54,42 +54,41 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti
const formatDate = (dateString: string) => new Date(dateString).toLocaleString();
// Load disposition codes from workflow configuration
useEffect(() => {
const loadDispositionCodes = async () => {
if (!accessToken) return;
try {
const response = await getWorkflowApiV1WorkflowFetchWorkflowIdGet({
path: { workflow_id: Number(workflowId) },
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const loadDispositionCodes = useCallback(async () => {
if (!accessToken) return;
try {
const response = await getWorkflowApiV1WorkflowFetchWorkflowIdGet({
path: { workflow_id: Number(workflowId) },
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const workflow = response.data;
if (workflow?.call_disposition_codes) {
// Update the disposition code attribute with actual options
const updatedAttributes = configuredAttributes.map(attr => {
if (attr.id === 'dispositionCode') {
return {
...attr,
config: {
...attr.config,
options: Object.keys(workflow.call_disposition_codes || {}).length > 0
? Object.keys(workflow.call_disposition_codes || {})
: [...DISPOSITION_CODES]
}
};
}
return attr;
});
setConfiguredAttributes(updatedAttributes);
}
} catch (err) {
console.error("Failed to load disposition codes:", err);
const workflow = response.data;
if (workflow?.call_disposition_codes) {
// Update the disposition code attribute with actual options
setConfiguredAttributes(prev => prev.map(attr => {
if (attr.id === 'dispositionCode') {
return {
...attr,
config: {
...attr.config,
options: Object.keys(workflow.call_disposition_codes || {}).length > 0
? Object.keys(workflow.call_disposition_codes || {})
: [...DISPOSITION_CODES]
}
};
}
return attr;
}));
}
};
loadDispositionCodes();
} catch (err) {
console.error("Failed to load disposition codes:", err);
}
}, [workflowId, accessToken]);
useEffect(() => {
loadDispositionCodes();
}, [loadDispositionCodes]);
const fetchWorkflowRuns = useCallback(async (page: number, filters?: ActiveFilter[]) => {
if (!accessToken) return;
try {

View file

@ -150,7 +150,7 @@ export const useWorkflowState = ({
initialTemplateContextVariables,
initialWorkflowConfigurations
);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}, []);
// Set up keyboard shortcuts for undo/redo
useEffect(() => {
@ -418,7 +418,7 @@ export const useWorkflowState = ({
// Validate workflow on mount
useEffect(() => {
validateWorkflow();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}, []);
return {
rfInstance,

View file

@ -65,7 +65,7 @@ export default function WorkflowDetailPage() {
const stickyTabs = workflow ? <WorkflowTabs workflowId={workflow.id} currentTab={currentTab} /> : null;
// Memoize user and getAccessToken to prevent unnecessary re-renders
const stableUser = useMemo(() => user, [user?.id]);
const stableUser = useMemo(() => user, [user]);
const stableGetAccessToken = useMemo(() => getAccessToken, [getAccessToken]);
if (loading) {

File diff suppressed because one or more lines are too long

View file

@ -234,6 +234,44 @@ export type DuplicateTemplateRequest = {
workflow_name: string;
};
/**
* Response model for embed configuration
*/
export type EmbedConfigResponse = {
workflow_id: number;
settings: {
[key: string]: unknown;
};
theme: string;
position: string;
button_text: string;
button_color: string;
};
export type EmbedTokenRequest = {
allowed_domains?: Array<string> | null;
settings?: {
[key: string]: unknown;
} | null;
usage_limit?: number | null;
expires_in_days?: number | null;
};
export type EmbedTokenResponse = {
id: number;
token: string;
allowed_domains: Array<string> | null;
settings: {
[key: string]: unknown;
} | null;
is_active: boolean;
usage_count: number;
usage_limit: number | null;
expires_at: string | null;
created_at: string;
embed_script: string;
};
export type FileMetadataResponse = {
key: string;
metadata: {
@ -261,6 +299,27 @@ export type ImpersonateResponse = {
access_token: string;
};
/**
* Request model for initializing an embed session
*/
export type InitEmbedRequest = {
token: string;
context_variables?: {
[key: string]: unknown;
} | null;
};
/**
* Response model for embed initialization
*/
export type InitEmbedResponse = {
session_token: string;
workflow_run_id: number;
config: {
[key: string]: unknown;
};
};
export type InitiateCallRequest = {
workflow_id: number;
workflow_run_id?: number | null;
@ -2888,6 +2947,220 @@ export type GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses = {
export type GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse = GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses[keyof GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponses];
export type OptionsInitApiV1PublicEmbedInitOptionsData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/public/embed/init';
};
export type OptionsInitApiV1PublicEmbedInitOptionsErrors = {
/**
* Not found
*/
404: unknown;
};
export type OptionsInitApiV1PublicEmbedInitOptionsResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type InitializeEmbedSessionApiV1PublicEmbedInitPostData = {
body: InitEmbedRequest;
path?: never;
query?: never;
url: '/api/v1/public/embed/init';
};
export type InitializeEmbedSessionApiV1PublicEmbedInitPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type InitializeEmbedSessionApiV1PublicEmbedInitPostError = InitializeEmbedSessionApiV1PublicEmbedInitPostErrors[keyof InitializeEmbedSessionApiV1PublicEmbedInitPostErrors];
export type InitializeEmbedSessionApiV1PublicEmbedInitPostResponses = {
/**
* Successful Response
*/
200: InitEmbedResponse;
};
export type InitializeEmbedSessionApiV1PublicEmbedInitPostResponse = InitializeEmbedSessionApiV1PublicEmbedInitPostResponses[keyof InitializeEmbedSessionApiV1PublicEmbedInitPostResponses];
export type GetEmbedConfigApiV1PublicEmbedConfigTokenGetData = {
body?: never;
path: {
token: string;
};
query?: never;
url: '/api/v1/public/embed/config/{token}';
};
export type GetEmbedConfigApiV1PublicEmbedConfigTokenGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetEmbedConfigApiV1PublicEmbedConfigTokenGetError = GetEmbedConfigApiV1PublicEmbedConfigTokenGetErrors[keyof GetEmbedConfigApiV1PublicEmbedConfigTokenGetErrors];
export type GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponses = {
/**
* Successful Response
*/
200: EmbedConfigResponse;
};
export type GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse = GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponses[keyof GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponses];
export type OptionsConfigApiV1PublicEmbedConfigTokenOptionsData = {
body?: never;
path: {
token: string;
};
query?: never;
url: '/api/v1/public/embed/config/{token}';
};
export type OptionsConfigApiV1PublicEmbedConfigTokenOptionsErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type OptionsConfigApiV1PublicEmbedConfigTokenOptionsError = OptionsConfigApiV1PublicEmbedConfigTokenOptionsErrors[keyof OptionsConfigApiV1PublicEmbedConfigTokenOptionsErrors];
export type OptionsConfigApiV1PublicEmbedConfigTokenOptionsResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData = {
body?: never;
headers?: {
authorization?: string | null;
};
path: {
workflow_id: number;
};
query?: never;
url: '/api/v1/workflow/{workflow_id}/embed-token';
};
export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError = DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteErrors[keyof DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteErrors];
export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponses = {
/**
* Successful Response
*/
200: {
[key: string]: unknown;
};
};
export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse = DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponses[keyof DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponses];
export type GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData = {
body?: never;
headers?: {
authorization?: string | null;
};
path: {
workflow_id: number;
};
query?: never;
url: '/api/v1/workflow/{workflow_id}/embed-token';
};
export type GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError = GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetErrors[keyof GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetErrors];
export type GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponses = {
/**
* Successful Response
*/
200: EmbedTokenResponse | null;
};
export type GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse = GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponses[keyof GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponses];
export type CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData = {
body: EmbedTokenRequest;
headers?: {
authorization?: string | null;
};
path: {
workflow_id: number;
};
query?: never;
url: '/api/v1/workflow/{workflow_id}/embed-token';
};
export type CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError = CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostErrors[keyof CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostErrors];
export type CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponses = {
/**
* Successful Response
*/
200: EmbedTokenResponse;
};
export type CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse = CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponses[keyof CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponses];
export type HealthApiV1HealthGetData = {
body?: never;
path?: never;

View file

@ -0,0 +1,66 @@
"use client"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsContent,TabsList, TabsTrigger }

View file

@ -80,8 +80,6 @@ export function UserConfigProvider({ children }: { children: ReactNode }) {
if (!auth.loading) {
fetchPermissions();
}
// We intentionally depend only on specific auth properties to avoid infinite loops
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [auth.loading, auth.provider, auth.getSelectedTeam, auth.listPermissions]);
@ -152,8 +150,6 @@ export function UserConfigProvider({ children }: { children: ReactNode }) {
} finally {
setLoading(false);
}
// We intentionally depend only on specific auth properties to avoid infinite loops
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [auth.loading, auth.isAuthenticated, auth.getAccessToken]);
useEffect(() => {