mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
feat: add text chat UI
This commit is contained in:
parent
e313f2d235
commit
f121147a42
14 changed files with 1465 additions and 294 deletions
|
|
@ -5,37 +5,60 @@ Revises: 4c1f1e3e8ef2
|
|||
Create Date: 2026-05-18 12:58:58.573381
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2f638891cbb6'
|
||||
down_revision: Union[str, None] = '4c1f1e3e8ef2'
|
||||
revision: str = "2f638891cbb6"
|
||||
down_revision: Union[str, None] = "4c1f1e3e8ef2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('workflow_run_text_sessions',
|
||||
sa.Column('workflow_run_id', sa.Integer(), nullable=False),
|
||||
sa.Column('revision', sa.Integer(), server_default=sa.text('0'), nullable=False),
|
||||
sa.Column('session_data', sa.JSON(), server_default=sa.text("'{}'::json"), nullable=False),
|
||||
sa.Column('checkpoint', sa.JSON(), server_default=sa.text("'{}'::json"), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['workflow_run_id'], ['workflow_runs.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('workflow_run_id')
|
||||
op.create_table(
|
||||
"workflow_run_text_sessions",
|
||||
sa.Column("workflow_run_id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"revision", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"session_data",
|
||||
sa.JSON(),
|
||||
server_default=sa.text("'{}'::json"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"checkpoint",
|
||||
sa.JSON(),
|
||||
server_default=sa.text("'{}'::json"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["workflow_run_id"], ["workflow_runs.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("workflow_run_id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_workflow_run_text_sessions_updated_at",
|
||||
"workflow_run_text_sessions",
|
||||
["updated_at"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index('ix_workflow_run_text_sessions_updated_at', 'workflow_run_text_sessions', ['updated_at'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('ix_workflow_run_text_sessions_updated_at', table_name='workflow_run_text_sessions')
|
||||
op.drop_table('workflow_run_text_sessions')
|
||||
op.drop_index(
|
||||
"ix_workflow_run_text_sessions_updated_at",
|
||||
table_name="workflow_run_text_sessions",
|
||||
)
|
||||
op.drop_table("workflow_run_text_sessions")
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
|||
|
|
@ -541,9 +541,7 @@ class WorkflowRunTextSessionModel(Base):
|
|||
onupdate=lambda: datetime.now(UTC),
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_workflow_run_text_sessions_updated_at", "updated_at"),
|
||||
)
|
||||
__table_args__ = (Index("ix_workflow_run_text_sessions_updated_at", "updated_at"),)
|
||||
|
||||
|
||||
class OrganizationUsageCycleModel(Base):
|
||||
|
|
|
|||
|
|
@ -148,11 +148,15 @@ def _build_response_from_run_and_session(workflow_run, text_session):
|
|||
)
|
||||
|
||||
|
||||
def _validate_turn_cursor(session_data: Dict[str, Any], cursor_turn_id: str | None) -> None:
|
||||
def _validate_turn_cursor(
|
||||
session_data: Dict[str, Any], cursor_turn_id: str | None
|
||||
) -> None:
|
||||
if cursor_turn_id is None:
|
||||
return
|
||||
if not any(turn.get("id") == cursor_turn_id for turn in session_data["turns"]):
|
||||
raise HTTPException(status_code=404, detail="Turn not found in text chat session")
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Turn not found in text chat session"
|
||||
)
|
||||
|
||||
|
||||
def _truncate_future_turns(
|
||||
|
|
@ -202,7 +206,9 @@ async def _load_text_session_or_404(
|
|||
if text_session.workflow_run.workflow_id != workflow_id:
|
||||
raise HTTPException(status_code=404, detail="Text chat session not found")
|
||||
if text_session.workflow_run.mode != WorkflowRunMode.TEXTCHAT.value:
|
||||
raise HTTPException(status_code=400, detail="Workflow run is not a text chat session")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Workflow run is not a text chat session"
|
||||
)
|
||||
return text_session
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -135,22 +135,3 @@
|
|||
.animate-spin-slow {
|
||||
animation: spin-slow 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Smaller Chatwoot bubble on workflow run pages */
|
||||
body.chatwoot-compact .woot--bubble-holder {
|
||||
transform: scale(0.7) !important;
|
||||
transform-origin: bottom right !important;
|
||||
right: 4px !important;
|
||||
bottom: 4px !important;
|
||||
}
|
||||
|
||||
body.chatwoot-compact .woot--bubble-holder .woot-widget-bubble {
|
||||
width: 48px !important;
|
||||
height: 48px !important;
|
||||
}
|
||||
|
||||
body.chatwoot-compact #chatwoot_live_chat_widget {
|
||||
bottom: 60px !important;
|
||||
right: 4px !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { createWorkflowDraftApiV1WorkflowWorkflowIdCreateDraftPost, getWorkflowV
|
|||
import type { DocumentResponseSchema, RecordingResponseSchema, ToolResponse } from '@/client/types.gen';
|
||||
import { FlowEdge, FlowNode, NodeType } from "@/components/flow/types";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { WorkflowConfigurations } from '@/types/workflow-configurations';
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ import { GenericNode } from "../../../components/flow/nodes/GenericNode";
|
|||
import { PhoneCallDialog } from './components/PhoneCallDialog';
|
||||
import { VersionHistoryPanel, WorkflowVersion } from './components/VersionHistoryPanel';
|
||||
import { WorkflowEditorHeader } from "./components/WorkflowEditorHeader";
|
||||
import { WorkflowTesterPanel } from './components/WorkflowTesterPanel';
|
||||
import { WorkflowProvider } from "./contexts/WorkflowContext";
|
||||
import { useWorkflowState } from "./hooks/useWorkflowState";
|
||||
import { layoutNodes } from './utils/layoutNodes';
|
||||
|
|
@ -65,6 +67,8 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
|
|||
const router = useRouter();
|
||||
const [isPhoneCallDialogOpen, setIsPhoneCallDialogOpen] = useState(false);
|
||||
const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false);
|
||||
const [isTesterRailOpen, setIsTesterRailOpen] = useState(true);
|
||||
const [isTesterSheetOpen, setIsTesterSheetOpen] = useState(false);
|
||||
const [versions, setVersions] = useState<WorkflowVersion[]>([]);
|
||||
const [versionsLoading, setVersionsLoading] = useState(false);
|
||||
const [versionsLoadingMore, setVersionsLoadingMore] = useState(false);
|
||||
|
|
@ -86,6 +90,7 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
|
|||
workflowName,
|
||||
isDirty,
|
||||
workflowValidationErrors,
|
||||
templateContextVariables,
|
||||
setNodes,
|
||||
setEdges,
|
||||
setIsDirty,
|
||||
|
|
@ -97,7 +102,6 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
|
|||
onConnect,
|
||||
onEdgesChange,
|
||||
onNodesChange,
|
||||
onRun,
|
||||
} = useWorkflowState({
|
||||
initialWorkflowName,
|
||||
workflowId,
|
||||
|
|
@ -248,6 +252,27 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
|
|||
return undefined;
|
||||
}, [activeVersionId, versions, currentVersionNumber, currentVersionStatus]);
|
||||
|
||||
const testerDisabledReason = useMemo(() => {
|
||||
if (isViewingHistoricalVersion) {
|
||||
return "Return to the draft before starting a new test session.";
|
||||
}
|
||||
if (isDirty) {
|
||||
return "Save the latest draft before testing so the session uses the workflow you are looking at.";
|
||||
}
|
||||
if (workflowValidationErrors.length > 0) {
|
||||
return "Resolve the current validation errors before starting another test.";
|
||||
}
|
||||
return null;
|
||||
}, [isDirty, isViewingHistoricalVersion, workflowValidationErrors.length]);
|
||||
|
||||
const handleOpenTester = useCallback(() => {
|
||||
if (window.innerWidth >= 1280) {
|
||||
setIsTesterRailOpen(true);
|
||||
return;
|
||||
}
|
||||
setIsTesterSheetOpen(true);
|
||||
}, []);
|
||||
|
||||
// Fetch documents, tools, and recordings once for the entire workflow
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
|
|
@ -343,12 +368,12 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
|
|||
isDirty={isDirty}
|
||||
workflowValidationErrors={workflowValidationErrors}
|
||||
rfInstance={rfInstance}
|
||||
onRun={onRun}
|
||||
workflowId={workflowId}
|
||||
workflowUuid={workflowUuid}
|
||||
saveWorkflow={guardedSaveWorkflow}
|
||||
user={user}
|
||||
onPhoneCallClick={() => setIsPhoneCallDialogOpen(true)}
|
||||
onTestAgentClick={handleOpenTester}
|
||||
onHistoryClick={handleOpenVersionPanel}
|
||||
activeVersionLabel={activeVersionLabel}
|
||||
isViewingHistoricalVersion={isViewingHistoricalVersion}
|
||||
|
|
@ -359,158 +384,181 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
|
|||
/>
|
||||
|
||||
{/* Workflow Canvas */}
|
||||
<div className="flex-1 relative">
|
||||
<ReactFlow
|
||||
key={activeVersionId ?? 'current'}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onConnect={isViewingHistoricalVersion ? undefined : onConnect}
|
||||
minZoom={0.4}
|
||||
onInit={(instance) => {
|
||||
rfInstance.current = instance;
|
||||
// Center the workflow on load
|
||||
setTimeout(() => {
|
||||
instance.fitView({ padding: 0.2, duration: 200, maxZoom: 0.75 });
|
||||
}, 0);
|
||||
}}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
defaultViewport={initialFlow?.viewport}
|
||||
nodesDraggable={!isViewingHistoricalVersion}
|
||||
nodesConnectable={!isViewingHistoricalVersion}
|
||||
edgesReconnectable={!isViewingHistoricalVersion}
|
||||
zoomOnDoubleClick={false}
|
||||
deleteKeyCode={isViewingHistoricalVersion ? null : "Backspace"}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={16}
|
||||
size={1}
|
||||
color="#94a3b8"
|
||||
/>
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="flex h-full min-w-0">
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<ReactFlow
|
||||
key={activeVersionId ?? 'current'}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onConnect={isViewingHistoricalVersion ? undefined : onConnect}
|
||||
minZoom={0.4}
|
||||
onInit={(instance) => {
|
||||
rfInstance.current = instance;
|
||||
// Center the workflow on load
|
||||
setTimeout(() => {
|
||||
instance.fitView({ padding: 0.2, duration: 200, maxZoom: 0.75 });
|
||||
}, 0);
|
||||
}}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
defaultViewport={initialFlow?.viewport}
|
||||
nodesDraggable={!isViewingHistoricalVersion}
|
||||
nodesConnectable={!isViewingHistoricalVersion}
|
||||
edgesReconnectable={!isViewingHistoricalVersion}
|
||||
zoomOnDoubleClick={false}
|
||||
deleteKeyCode={isViewingHistoricalVersion ? null : "Backspace"}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={16}
|
||||
size={1}
|
||||
color="#94a3b8"
|
||||
/>
|
||||
|
||||
{/* Top-right controls - vertical layout (hidden when viewing history) */}
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Panel position="top-right">
|
||||
{/* Top-right controls - vertical layout (hidden when viewing history) */}
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Panel position="top-right">
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
size="icon"
|
||||
onClick={() => setIsAddNodePanelOpen(true)}
|
||||
className="shadow-md hover:shadow-lg"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Add node</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => router.push(`/workflow/${workflowId}/settings`)}
|
||||
className="bg-white shadow-sm hover:shadow-md"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Workflow settings</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
|
||||
{/* Bottom-left controls - horizontal layout with custom buttons */}
|
||||
<div className="absolute bottom-12 left-8 z-10 flex gap-2">
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
size="icon"
|
||||
onClick={() => setIsAddNodePanelOpen(true)}
|
||||
className="shadow-md hover:shadow-lg"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Add node</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => rfInstance.current?.zoomIn()}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Zoom in</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => rfInstance.current?.zoomOut()}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Zoom out</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => rfInstance.current?.fitView()}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Fit view</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => router.push(`/workflow/${workflowId}/settings`)}
|
||||
className="bg-white shadow-sm hover:shadow-md"
|
||||
onClick={() => {
|
||||
setNodes(layoutNodes(nodes, edges, 'TB', rfInstance));
|
||||
setIsDirty(true);
|
||||
}}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<BrushCleaning className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Workflow settings</p>
|
||||
<TooltipContent side="top">
|
||||
<p>Tidy Up</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTesterRailOpen && (
|
||||
<aside className="hidden h-full w-[420px] shrink-0 border-l border-border xl:block">
|
||||
<WorkflowTesterPanel
|
||||
workflowId={workflowId}
|
||||
initialContextVariables={templateContextVariables}
|
||||
disabled={testerDisabledReason !== null}
|
||||
disabledReason={testerDisabledReason}
|
||||
onClose={() => setIsTesterRailOpen(false)}
|
||||
/>
|
||||
</aside>
|
||||
)}
|
||||
</ReactFlow>
|
||||
|
||||
{/* Bottom-left controls - horizontal layout with custom buttons */}
|
||||
<div className="absolute bottom-12 left-8 z-10 flex gap-2">
|
||||
<TooltipProvider>
|
||||
{/* Zoom In */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => rfInstance.current?.zoomIn()}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Zoom in</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Zoom Out */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => rfInstance.current?.zoomOut()}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Zoom out</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Fit View */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => rfInstance.current?.fitView()}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Fit view</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Tidy/Arrange Nodes (hidden when viewing history) */}
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setNodes(layoutNodes(nodes, edges, 'TB', rfInstance));
|
||||
setIsDirty(true);
|
||||
}}
|
||||
className="bg-white shadow-sm hover:shadow-md h-8 w-8"
|
||||
>
|
||||
<BrushCleaning className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Tidy Up</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<Sheet open={isTesterSheetOpen} onOpenChange={setIsTesterSheetOpen}>
|
||||
<SheetContent side="right" className="w-full max-w-none p-0 sm:max-w-xl xl:hidden">
|
||||
<WorkflowTesterPanel
|
||||
workflowId={workflowId}
|
||||
initialContextVariables={templateContextVariables}
|
||||
disabled={testerDisabledReason !== null}
|
||||
disabledReason={testerDisabledReason}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<AddNodePanel
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { ReactFlowInstance } from "@xyflow/react";
|
||||
import { AlertCircle, ArrowLeft, ChevronDown, Clipboard, Copy, Download, Eye, History, LoaderCircle, Menu, MoreVertical, Pencil, Phone, Rocket } from "lucide-react";
|
||||
import { AlertCircle, ArrowLeft, Bot, Clipboard, Copy, Download, Eye, History, LoaderCircle, Menu, MoreVertical, Pencil, Phone, Rocket } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import posthog from "posthog-js";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
|
@ -28,20 +27,18 @@ import {
|
|||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { PostHogEvent } from "@/constants/posthog-events";
|
||||
import { WORKFLOW_RUN_MODES } from "@/constants/workflowRunModes";
|
||||
|
||||
interface WorkflowEditorHeaderProps {
|
||||
workflowName: string;
|
||||
isDirty: boolean;
|
||||
workflowValidationErrors: WorkflowError[];
|
||||
rfInstance: React.RefObject<ReactFlowInstance<FlowNode, FlowEdge> | null>;
|
||||
onRun: (mode: string) => Promise<void>;
|
||||
workflowId: number;
|
||||
workflowUuid?: string;
|
||||
saveWorkflow: (updateWorkflowDefinition?: boolean) => Promise<void>;
|
||||
user: { id: string; email?: string };
|
||||
onPhoneCallClick: () => void;
|
||||
onTestAgentClick: () => void;
|
||||
onHistoryClick: () => void;
|
||||
activeVersionLabel?: string;
|
||||
isViewingHistoricalVersion: boolean;
|
||||
|
|
@ -57,8 +54,8 @@ export const WorkflowEditorHeader = ({
|
|||
workflowValidationErrors,
|
||||
rfInstance,
|
||||
saveWorkflow,
|
||||
onRun,
|
||||
onPhoneCallClick,
|
||||
onTestAgentClick,
|
||||
onHistoryClick,
|
||||
activeVersionLabel,
|
||||
isViewingHistoricalVersion,
|
||||
|
|
@ -301,7 +298,7 @@ export const WorkflowEditorHeader = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right section: Version + Unsaved indicator + Call button + Save button */}
|
||||
{/* Right section: Version + status + tester/call actions + save */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Read-only banner when viewing a historical version */}
|
||||
{isViewingHistoricalVersion && (
|
||||
|
|
@ -388,48 +385,25 @@ export const WorkflowEditorHeader = ({
|
|||
</Popover>
|
||||
)}
|
||||
|
||||
{/* Call button with dropdown (hidden when viewing history) */}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 bg-transparent border-[#3a3a3a] hover:bg-[#2a2a2a] text-white"
|
||||
onClick={onTestAgentClick}
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
Test Agent
|
||||
</Button>
|
||||
|
||||
{!isViewingHistoricalVersion && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 bg-transparent border-[#3a3a3a] hover:bg-[#2a2a2a] text-white"
|
||||
disabled={isCallDisabled}
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
Call
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="bg-[#1a1a1a] border-[#3a3a3a]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
posthog.capture(PostHogEvent.WEB_CALL_INITIATED, {
|
||||
workflow_id: workflowId,
|
||||
workflow_name: workflowName,
|
||||
});
|
||||
onRun(WORKFLOW_RUN_MODES.SMALL_WEBRTC);
|
||||
}}
|
||||
className="text-white hover:bg-[#2a2a2a] cursor-pointer"
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
Web Call
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
// Delay opening dialog to next event cycle to allow DropdownMenu
|
||||
// to clean up first, preventing pointer-events: none stuck on body
|
||||
// See: https://github.com/radix-ui/primitives/issues/1241
|
||||
setTimeout(onPhoneCallClick, 0);
|
||||
}}
|
||||
className="text-white hover:bg-[#2a2a2a] cursor-pointer"
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
Phone Call
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 bg-transparent border-[#3a3a3a] hover:bg-[#2a2a2a] text-white"
|
||||
disabled={isCallDisabled}
|
||||
onClick={onPhoneCallClick}
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
Phone Call
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Save button (only shown when editing the draft) */}
|
||||
|
|
|
|||
1008
ui/src/app/workflow/[workflowId]/components/WorkflowTesterPanel.tsx
Normal file
1008
ui/src/app/workflow/[workflowId]/components/WorkflowTesterPanel.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -13,8 +13,11 @@ import {
|
|||
RealtimeFeedback,
|
||||
WorkflowConfigErrorDialog
|
||||
} from "./components";
|
||||
import { TranscriptRailFrame } from "./components/shared/TranscriptRailFrame";
|
||||
import { useWebSocketRTC } from "./hooks";
|
||||
|
||||
const RUN_SHELL_HEIGHT_CLASS = "h-[calc(100svh-49px)] min-h-[calc(100svh-49px)] max-h-[calc(100svh-49px)]";
|
||||
|
||||
const BrowserCall = ({ workflowId, workflowRunId, initialContextVariables }: {
|
||||
workflowId: number,
|
||||
workflowRunId: number,
|
||||
|
|
@ -106,10 +109,9 @@ const BrowserCall = ({ workflowId, workflowRunId, initialContextVariables }: {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen w-full overflow-hidden">
|
||||
{/* Main content - 2/3 width when panel visible, full width otherwise */}
|
||||
<div className="w-2/3 h-full overflow-y-auto">
|
||||
<div className="flex justify-center items-center h-full px-8">
|
||||
<div className={`flex ${RUN_SHELL_HEIGHT_CLASS} min-h-0 w-full overflow-hidden bg-background`}>
|
||||
<div className="min-w-0 flex-1 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center px-8 py-8">
|
||||
<Card className="w-full max-w-xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Call Voice Agent</CardTitle>
|
||||
|
|
@ -151,14 +153,15 @@ const BrowserCall = ({ workflowId, workflowRunId, initialContextVariables }: {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show transcript panel */}
|
||||
<div className="w-1/3 h-full shrink-0 overflow-hidden">
|
||||
<RealtimeFeedback
|
||||
mode="live"
|
||||
messages={feedbackMessages}
|
||||
isCallActive={connectionActive}
|
||||
isCallCompleted={isCompleted}
|
||||
/>
|
||||
<div className="h-full min-h-0 w-[420px] shrink-0 border-l border-border bg-background p-5">
|
||||
<TranscriptRailFrame className="h-full">
|
||||
<RealtimeFeedback
|
||||
mode="live"
|
||||
messages={feedbackMessages}
|
||||
isCallActive={connectionActive}
|
||||
isCallCompleted={isCompleted}
|
||||
/>
|
||||
</TranscriptRailFrame>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,31 +42,33 @@ export function TranscriptContainer({
|
|||
const StatusIcon = statusConfig.icon;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col bg-background border-l border-border">
|
||||
<div className="flex h-full min-h-0 w-full flex-col bg-background">
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b border-border shrink-0">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="font-medium text-sm whitespace-nowrap">{title}</span>
|
||||
<div className={cn(
|
||||
"flex items-center gap-1 text-xs px-2 py-0.5 rounded-full shrink-0",
|
||||
statusConfig.className
|
||||
)}>
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
<span>{statusConfig.label}</span>
|
||||
<div className="shrink-0 border-b border-border px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<MessageSquare className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-sm font-medium whitespace-nowrap">{title}</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{messageCount !== undefined && messageCount > 0 ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{messageCount} messages
|
||||
</span>
|
||||
) : null}
|
||||
<div className={cn(
|
||||
"flex items-center gap-1 rounded-full px-2 py-0.5 text-xs shrink-0",
|
||||
statusConfig.className
|
||||
)}>
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
<span>{statusConfig.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{children}
|
||||
|
||||
{/* Footer with message count */}
|
||||
{messageCount !== undefined && messageCount > 0 && (
|
||||
<div className="px-4 py-2 border-t border-border text-xs text-muted-foreground shrink-0">
|
||||
{messageCount} messages
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function TranscriptMessage({ message, nextMessage }: TranscriptMessagePro
|
|||
return (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<div className="flex-1 h-px bg-border"></div>
|
||||
<div className="px-2 py-1 rounded-md text-xs bg-blue-500/10 border border-blue-500/20 inline-flex items-center gap-1.5">
|
||||
<div className="inline-flex items-center gap-1.5 rounded-full border border-blue-500/20 bg-blue-500/10 px-3 py-1 text-xs">
|
||||
<GitBranch className="h-3 w-3 text-blue-500" />
|
||||
<span className="font-medium text-blue-700 dark:text-blue-400">
|
||||
{message.nodeName}
|
||||
|
|
@ -98,7 +98,7 @@ export function TranscriptMessage({ message, nextMessage }: TranscriptMessagePro
|
|||
<span>{(ttfbMetric.ttfbSeconds * 1000).toFixed(0)}ms</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-3 py-1.5 rounded-full text-xs bg-amber-500/10 border border-amber-500/20 inline-flex items-center gap-2">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-amber-500/20 bg-amber-500/10 px-3 py-1.5 text-xs">
|
||||
<Wrench className="h-3 w-3 text-amber-500" />
|
||||
<span className="font-mono text-amber-700 dark:text-amber-400">
|
||||
{message.functionName}()
|
||||
|
|
@ -120,7 +120,7 @@ export function TranscriptMessage({ message, nextMessage }: TranscriptMessagePro
|
|||
"flex",
|
||||
isUser ? "justify-end" : "justify-start"
|
||||
)}>
|
||||
<div className="flex flex-col gap-1 max-w-[85%]">
|
||||
<div className="flex max-w-[85%] flex-col gap-1">
|
||||
{/* Show TTFB metric above bot messages */}
|
||||
{ttfbMetric && ttfbMetric.ttfbSeconds !== undefined && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground px-1">
|
||||
|
|
@ -131,10 +131,10 @@ export function TranscriptMessage({ message, nextMessage }: TranscriptMessagePro
|
|||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"px-3 py-2 rounded-2xl text-sm",
|
||||
"rounded-2xl px-4 py-3 text-sm shadow-sm",
|
||||
isUser
|
||||
? "bg-primary text-primary-foreground rounded-br-md"
|
||||
: "bg-muted rounded-bl-md",
|
||||
: "bg-muted rounded-bl-md border border-slate-200/80",
|
||||
!message.final && "opacity-70"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
'use client';
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TranscriptRailFrameProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
export function TranscriptRailFrame({
|
||||
children,
|
||||
className,
|
||||
header,
|
||||
footer,
|
||||
}: TranscriptRailFrameProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'min-h-0 flex h-full flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm',
|
||||
className,
|
||||
)}>
|
||||
{header ? (
|
||||
<div className="shrink-0 border-b border-border px-4 py-3">
|
||||
{header}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
{footer ? (
|
||||
<div className="shrink-0 border-t border-border px-4 py-3">
|
||||
{footer}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||
|
||||
import BrowserCall from '@/app/workflow/[workflowId]/run/[runId]/BrowserCall';
|
||||
import { RealtimeFeedback, WorkflowRunLogs } from '@/app/workflow/[workflowId]/run/[runId]/components/RealtimeFeedback';
|
||||
import { TranscriptRailFrame } from '@/app/workflow/[workflowId]/run/[runId]/components/shared/TranscriptRailFrame';
|
||||
import WorkflowLayout from '@/app/workflow/WorkflowLayout';
|
||||
import {
|
||||
createWorkflowRunApiV1WorkflowWorkflowIdRunsPost,
|
||||
|
|
@ -29,12 +30,83 @@ interface WorkflowRunResponse {
|
|||
is_completed: boolean;
|
||||
transcript_url: string | null;
|
||||
recording_url: string | null;
|
||||
cost_info: {
|
||||
dograh_token_usage?: number | null;
|
||||
call_duration_seconds?: number | null;
|
||||
} | null;
|
||||
initial_context: Record<string, string | number | boolean | object> | null;
|
||||
gathered_context: Record<string, string | number | boolean | object> | null;
|
||||
logs: WorkflowRunLogs | null;
|
||||
annotations: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const RUN_SHELL_HEIGHT_CLASS = "h-[calc(100svh-49px)] min-h-[calc(100svh-49px)] max-h-[calc(100svh-49px)]";
|
||||
|
||||
function formatDuration(seconds?: number | null) {
|
||||
if (seconds == null || Number.isNaN(seconds)) return 'N/A';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
if (mins === 0) return `${secs}s`;
|
||||
return `${mins}m ${secs}s`;
|
||||
}
|
||||
|
||||
function getTranscriptMetrics(logs: WorkflowRunLogs | null, gatheredContext: Record<string, string | number | boolean | object> | null) {
|
||||
const events = logs?.realtime_feedback_events ?? [];
|
||||
const userTurns = events.filter((event) => event.type === 'rtf-user-transcription' && event.payload.final).length;
|
||||
const botTurns = events.filter((event) => event.type === 'rtf-bot-text').length;
|
||||
const toolCalls = events.filter((event) => event.type === 'rtf-function-call-end').length;
|
||||
const nodeNames = new Set(
|
||||
events
|
||||
.map((event) => event.payload.node_name)
|
||||
.filter((nodeName): nodeName is string => Boolean(nodeName))
|
||||
);
|
||||
const visitedNodes = Array.isArray(gatheredContext?.nodes_visited)
|
||||
? gatheredContext.nodes_visited.length
|
||||
: nodeNames.size;
|
||||
|
||||
return { userTurns, botTurns, toolCalls, visitedNodes };
|
||||
}
|
||||
|
||||
function MetricCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-muted/40 px-4 py-3">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.14em] text-muted-foreground">{label}</p>
|
||||
<p className="mt-2 text-lg font-semibold text-foreground">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RunMetricsSection({
|
||||
costInfo,
|
||||
logs,
|
||||
gatheredContext,
|
||||
}: {
|
||||
costInfo: WorkflowRunResponse['cost_info'];
|
||||
logs: WorkflowRunLogs | null;
|
||||
gatheredContext: Record<string, string | number | boolean | object> | null;
|
||||
}) {
|
||||
const metrics = getTranscriptMetrics(logs, gatheredContext);
|
||||
|
||||
return (
|
||||
<Card className="border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Run Metrics</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<MetricCard label="Duration" value={formatDuration(costInfo?.call_duration_seconds)} />
|
||||
<MetricCard
|
||||
label="Token Usage"
|
||||
value={costInfo?.dograh_token_usage != null ? costInfo.dograh_token_usage.toLocaleString() : 'N/A'}
|
||||
/>
|
||||
<MetricCard label="User Turns" value={String(metrics.userTurns)} />
|
||||
<MetricCard label="Bot Turns" value={String(metrics.botTurns)} />
|
||||
<MetricCard label="Tool Calls" value={String(metrics.toolCalls)} />
|
||||
<MetricCard label="Nodes Visited" value={String(metrics.visitedNodes)} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextDisplay({ title, context }: { title: string; context: Record<string, string | number | boolean | object> | null }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
|
|
@ -94,12 +166,6 @@ export default function WorkflowRunPage() {
|
|||
}
|
||||
}, [auth]);
|
||||
|
||||
// Shrink and reposition Chatwoot bubble on this page
|
||||
useEffect(() => {
|
||||
document.body.classList.add('chatwoot-compact');
|
||||
return () => document.body.classList.remove('chatwoot-compact');
|
||||
}, []);
|
||||
|
||||
const { openPreview, dialog } = MediaPreviewDialog();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -120,6 +186,7 @@ export default function WorkflowRunPage() {
|
|||
is_completed: response.data?.is_completed ?? false,
|
||||
transcript_url: response.data?.transcript_url ?? null,
|
||||
recording_url: response.data?.recording_url ?? null,
|
||||
cost_info: response.data?.cost_info ?? null,
|
||||
initial_context: response.data?.initial_context as Record<string, string> | null ?? null,
|
||||
gathered_context: response.data?.gathered_context as Record<string, string> | null ?? null,
|
||||
logs: response.data?.logs as WorkflowRunLogs | null ?? null,
|
||||
|
|
@ -181,10 +248,9 @@ export default function WorkflowRunPage() {
|
|||
}
|
||||
else if (workflowRun?.is_completed) {
|
||||
returnValue = (
|
||||
<div className="flex h-screen w-full overflow-hidden">
|
||||
{/* Main content - 2/3 width */}
|
||||
<div className="w-2/3 h-full overflow-y-auto">
|
||||
<div className="w-full max-w-4xl space-y-6 p-6">
|
||||
<div className={`flex ${RUN_SHELL_HEIGHT_CLASS} min-h-0 w-full overflow-hidden bg-background`}>
|
||||
<div className="min-w-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-4xl space-y-6 p-6">
|
||||
<Card className="border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
@ -285,6 +351,12 @@ export default function WorkflowRunPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<RunMetricsSection
|
||||
costInfo={workflowRun?.cost_info}
|
||||
logs={workflowRun?.logs}
|
||||
gatheredContext={workflowRun?.gathered_context}
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<ContextDisplay
|
||||
title="Initial Context"
|
||||
|
|
@ -305,33 +377,32 @@ export default function WorkflowRunPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transcript panel - 1/3 width */}
|
||||
<div className="w-1/3 h-full shrink-0 overflow-hidden">
|
||||
<RealtimeFeedback mode="historical" logs={workflowRun?.logs} />
|
||||
<div className="h-full min-h-0 w-[420px] shrink-0 border-l border-border bg-background p-5">
|
||||
<TranscriptRailFrame className="h-full">
|
||||
<RealtimeFeedback mode="historical" logs={workflowRun?.logs} />
|
||||
</TranscriptRailFrame>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
returnValue =
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<BrowserCall
|
||||
workflowId={Number(params.workflowId)}
|
||||
workflowRunId={Number(params.runId)}
|
||||
initialContextVariables={
|
||||
workflowRun?.initial_context
|
||||
? Object.fromEntries(
|
||||
Object.entries(workflowRun.initial_context).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === 'object' && value !== null
|
||||
? JSON.stringify(value)
|
||||
: String(value)
|
||||
])
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<BrowserCall
|
||||
workflowId={Number(params.workflowId)}
|
||||
workflowRunId={Number(params.runId)}
|
||||
initialContextVariables={
|
||||
workflowRun?.initial_context
|
||||
? Object.fromEntries(
|
||||
Object.entries(workflowRun.initial_context).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === 'object' && value !== null
|
||||
? JSON.stringify(value)
|
||||
: String(value)
|
||||
])
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
declare global {
|
||||
|
|
@ -22,7 +23,22 @@ const CHATWOOT_BASE_URL = process.env.NEXT_PUBLIC_CHATWOOT_URL;
|
|||
const CHATWOOT_WEBSITE_TOKEN = process.env.NEXT_PUBLIC_CHATWOOT_TOKEN;
|
||||
|
||||
export default function ChatwootWidget() {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const isWorkflowRunPage = /^\/workflow\/[^/]+\/run\/[^/]+(?:\/.*)?$/.test(pathname);
|
||||
|
||||
if (isWorkflowRunPage) {
|
||||
document.getElementById("cw-widget-holder")?.remove();
|
||||
document.getElementById("cw-bubble-holder")?.remove();
|
||||
document.getElementById("cw-widget-styles")?.remove();
|
||||
document
|
||||
.querySelector(`script[src="${CHATWOOT_BASE_URL}/packs/js/sdk.js"]`)
|
||||
?.remove();
|
||||
delete window.chatwootSettings;
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't initialize if environment variables are not set
|
||||
if (!CHATWOOT_BASE_URL || !CHATWOOT_WEBSITE_TOKEN) {
|
||||
console.warn("Chatwoot not configured: Missing NEXT_PUBLIC_CHATWOOT_URL or NEXT_PUBLIC_CHATWOOT_TOKEN");
|
||||
|
|
@ -72,7 +88,7 @@ export default function ChatwootWidget() {
|
|||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
}, []);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const WORKFLOW_RUN_MODES = {
|
|||
CLOUDONIX: 'cloudonix',
|
||||
WEBRTC: 'webrtc',
|
||||
SMALL_WEBRTC: 'smallwebrtc',
|
||||
TEXTCHAT: 'textchat',
|
||||
ARI: 'ari',
|
||||
TELNYX: 'telnyx',
|
||||
PLIVO: 'plivo'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue