diff --git a/ui/package-lock.json b/ui/package-lock.json index 01c5fad..9923bb0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -26,7 +26,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@sentry/nextjs": "^9.28.1", "@stackframe/stack": "^2.8.80", - "@xyflow/react": "^12.9.2", + "@xyflow/react": "^12.10.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -11227,12 +11227,12 @@ "peer": true }, "node_modules/@xyflow/react": { - "version": "12.9.2", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.9.2.tgz", - "integrity": "sha512-Xr+LFcysHCCoc5KRHaw+FwbqbWYxp9tWtk1mshNcqy25OAPuaKzXSdqIMNOA82TIXF/gFKo0Wgpa6PU7wUUVqw==", + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", "license": "MIT", "dependencies": { - "@xyflow/system": "0.0.72", + "@xyflow/system": "0.0.76", "classcat": "^5.0.3", "zustand": "^4.4.0" }, @@ -11270,9 +11270,9 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.72", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.72.tgz", - "integrity": "sha512-WBI5Aau0fXTXwxHPzceLNS6QdXggSWnGjDtj/gG669crApN8+SCmEtkBth1m7r6pStNo/5fI9McEi7Dk0ymCLA==", + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -11661,12 +11661,6 @@ "tslib": "^2.4.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -11702,17 +11696,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -11957,6 +11940,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -12243,18 +12227,6 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -12797,15 +12769,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -12875,6 +12838,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -13022,6 +12986,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13031,6 +12996,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13075,6 +13041,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -13087,6 +13054,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13908,26 +13876,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -13944,22 +13892,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", @@ -14048,6 +13980,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -14081,6 +14014,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -14192,6 +14126,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14268,6 +14203,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14280,6 +14216,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -15084,12 +15021,6 @@ "license": "MIT", "peer": true }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -15499,6 +15430,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -15547,6 +15479,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -15556,6 +15489,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -18131,18 +18065,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", diff --git a/ui/package.json b/ui/package.json index a2bda20..4a4006a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,7 +30,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@sentry/nextjs": "^9.28.1", "@stackframe/stack": "^2.8.80", - "@xyflow/react": "^12.9.2", + "@xyflow/react": "^12.10.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx b/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx index b9a35f2..19b7ada 100644 --- a/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx +++ b/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx @@ -17,6 +17,7 @@ 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 { useOnboarding } from '@/context/OnboardingContext'; import { WorkflowConfigurations } from '@/types/workflow-configurations'; import AddNodePanel from "../../../components/flow/AddNodePanel"; @@ -24,6 +25,7 @@ import CustomEdge from "../../../components/flow/edges/CustomEdge"; import { GenericNode } from "../../../components/flow/nodes/GenericNode"; import { PhoneCallDialog } from './components/PhoneCallDialog'; import { VersionHistoryPanel, WorkflowVersion } from './components/VersionHistoryPanel'; +import type { WorkflowRuntimeFocusMode, WorkflowRuntimeNodeTransition } from './components/workflow-tester/types'; import { WorkflowEditorHeader } from "./components/WorkflowEditorHeader"; import { WorkflowTesterPanel } from './components/WorkflowTesterPanel'; import { WorkflowProvider } from "./contexts/WorkflowContext"; @@ -40,6 +42,8 @@ interface RenderWorkflowProps { initialWorkflowName: string; workflowId: number; workflowUuid?: string; + initialTotalRuns?: number | null; + openTesterOnLoad?: boolean; initialFlow?: { nodes: FlowNode[]; edges: FlowEdge[]; @@ -56,18 +60,33 @@ interface RenderWorkflowProps { user: { id: string; email?: string }; } -function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initialFlow, initialTemplateContextVariables, initialWorkflowConfigurations, initialVersionNumber, initialVersionStatus, user }: RenderWorkflowProps) { +function RenderWorkflow({ + initialWorkflowName, + workflowId, + workflowUuid, + initialTotalRuns, + openTesterOnLoad = false, + initialFlow, + initialTemplateContextVariables, + initialWorkflowConfigurations, + initialVersionNumber, + initialVersionStatus, + user, +}: RenderWorkflowProps) { const router = useRouter(); const { specs } = useNodeSpecs(); + const { hasCompletedAction } = useOnboarding(); const [isPhoneCallDialogOpen, setIsPhoneCallDialogOpen] = useState(false); const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false); const [isTesterRailOpen, setIsTesterRailOpen] = useState(true); const [isTesterSheetOpen, setIsTesterSheetOpen] = useState(false); + const [isDesktopViewport, setIsDesktopViewport] = useState(false); const [versions, setVersions] = useState([]); const [versionsLoading, setVersionsLoading] = useState(false); const [versionsLoadingMore, setVersionsLoadingMore] = useState(false); const [versionsHasMore, setVersionsHasMore] = useState(false); const [activeVersionId, setActiveVersionId] = useState(null); + const hasAutoOpenedTester = useRef(false); // Version info that updates immediately from the GET/save/publish responses. const [currentVersionNumber, setCurrentVersionNumber] = useState(initialVersionNumber ?? null); const [currentVersionStatus, setCurrentVersionStatus] = useState(initialVersionStatus ?? null); @@ -75,6 +94,9 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial const [documents, setDocuments] = useState(undefined); const [tools, setTools] = useState(undefined); const [recordings, setRecordings] = useState([]); + const [runtimeFocusMode, setRuntimeFocusMode] = useState("follow"); + const [activeRuntimeNodeId, setActiveRuntimeNodeId] = useState(null); + const [runtimePulseNonce, setRuntimePulseNonce] = useState(0); const { rfInstance, @@ -208,6 +230,13 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial return true; }, [activeVersionId, versions, hasDraft]); + useEffect(() => { + if (!isViewingHistoricalVersion) { + return; + } + setActiveRuntimeNodeId(null); + }, [isViewingHistoricalVersion]); + // Return to the draft version, creating one from published if needed const handleBackToDraft = useCallback(async () => { const existingDraft = versions.find((v) => v.status === "draft"); @@ -283,6 +312,29 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial setIsTesterSheetOpen(true); }, []); + const shouldShowWebCallOnboarding = useMemo(() => { + return (initialTotalRuns ?? 0) === 0 && !hasCompletedAction('web_call_started'); + }, [hasCompletedAction, initialTotalRuns]); + + useEffect(() => { + const syncViewport = () => { + setIsDesktopViewport(window.innerWidth >= 1280); + }; + + syncViewport(); + window.addEventListener('resize', syncViewport); + return () => window.removeEventListener('resize', syncViewport); + }, []); + + useEffect(() => { + if (hasAutoOpenedTester.current || !openTesterOnLoad || !shouldShowWebCallOnboarding || testerDisabledReason) { + return; + } + + handleOpenTester(); + hasAutoOpenedTester.current = true; + }, [handleOpenTester, openTesterOnLoad, shouldShowWebCallOnboarding, testerDisabledReason]); + // Fetch documents, tools, and recordings once for the entire workflow useEffect(() => { const fetchData = async () => { @@ -326,6 +378,48 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial type: "custom" }), []); + const displayNodes = useMemo( + () => + nodes.map((node) => + node.id === activeRuntimeNodeId + ? { + ...node, + data: { + ...node.data, + runtime_active: true, + runtime_pulse_nonce: runtimePulseNonce, + }, + } + : node, + ), + [activeRuntimeNodeId, nodes, runtimePulseNonce], + ); + + const handleRuntimeNodeTransition = useCallback( + (transition: WorkflowRuntimeNodeTransition) => { + const nodeId = transition.nodeId; + const instance = rfInstance.current; + if (!nodeId || !instance) { + return; + } + + setActiveRuntimeNodeId(nodeId); + setRuntimePulseNonce((value) => value + 1); + + if (runtimeFocusMode !== "follow" || !instance.viewportInitialized) { + return; + } + + void instance.fitView({ + nodes: [{ id: nodeId }], + duration: 350, + padding: 0.45, + maxZoom: 0.9, + }); + }, + [rfInstance, runtimeFocusMode], + ); + // Guard saveWorkflow so it's a no-op when viewing a historical version. // This is the single safety net that covers every save path: header button, // Cmd+S, node edit dialogs, stale doc/tool cleanup, etc. @@ -418,7 +512,7 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
setIsTesterRailOpen(false)} + runtimeFocusMode={runtimeFocusMode} + onRuntimeFocusModeChange={setRuntimeFocusMode} + onRuntimeNodeTransition={handleRuntimeNodeTransition} /> )} @@ -585,6 +684,11 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial initialContextVariables={templateContextVariables} disabled={testerDisabledReason !== null} disabledReason={testerDisabledReason} + showWebCallOnboarding={shouldShowWebCallOnboarding} + isVisible={isTesterSheetOpen} + runtimeFocusMode={runtimeFocusMode} + onRuntimeFocusModeChange={setRuntimeFocusMode} + onRuntimeNodeTransition={handleRuntimeNodeTransition} /> diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowTesterPanel.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowTesterPanel.tsx index c7d7d91..97a53f3 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowTesterPanel.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowTesterPanel.tsx @@ -1,21 +1,26 @@ "use client"; import { Loader2, MessageSquareText, Mic, Phone, RefreshCw, X } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; +import posthog from "posthog-js"; +import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { createWorkflowRunApiV1WorkflowWorkflowIdRunsPost } from "@/client/sdk.gen"; +import { OnboardingTooltip } from "@/components/onboarding/OnboardingTooltip"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { PostHogEvent } from "@/constants/posthog-events"; import { WORKFLOW_RUN_MODES } from "@/constants/workflowRunModes"; +import { useOnboarding } from "@/context/OnboardingContext"; import { useAuth } from "@/lib/auth"; import { cn, getRandomId } from "@/lib/utils"; import { AiSimulatorPlaceholder } from "./workflow-tester/AiSimulatorPlaceholder"; import { EmbeddedVoiceTester } from "./workflow-tester/EmbeddedVoiceTester"; import { ManualTextChatPanel } from "./workflow-tester/ManualTextChatPanel"; -import { ChatModeToggle, DisabledNotice, EmptyState } from "./workflow-tester/shared"; +import { ChatModeToggle, DisabledNotice, EmptyState, RuntimeFocusToggle } from "./workflow-tester/shared"; +import type { WorkflowRuntimeFocusMode, WorkflowRuntimeNodeTransition } from "./workflow-tester/types"; import { extractSdkErrorMessage, getErrorMessage } from "./workflow-tester/utils"; interface WorkflowTesterPanelProps { @@ -23,8 +28,13 @@ interface WorkflowTesterPanelProps { initialContextVariables?: Record; disabled: boolean; disabledReason: string | null; + showWebCallOnboarding?: boolean; + isVisible?: boolean; className?: string; onClose?: () => void; + runtimeFocusMode: WorkflowRuntimeFocusMode; + onRuntimeFocusModeChange: (mode: WorkflowRuntimeFocusMode) => void; + onRuntimeNodeTransition?: (transition: WorkflowRuntimeNodeTransition) => void; } export function WorkflowTesterPanel({ @@ -32,10 +42,16 @@ export function WorkflowTesterPanel({ initialContextVariables, disabled, disabledReason, + showWebCallOnboarding = false, + isVisible = true, className, onClose, + runtimeFocusMode, + onRuntimeFocusModeChange, + onRuntimeNodeTransition, }: WorkflowTesterPanelProps) { const auth = useAuth(); + const { hasSeenTooltip, markTooltipSeen, markActionCompleted } = useOnboarding(); const { isAuthenticated, loading: authLoading, getAccessToken } = auth; const [accessToken, setAccessToken] = useState(null); const [activeMode, setActiveMode] = useState<"audio" | "text">("audio"); @@ -45,6 +61,7 @@ export function WorkflowTesterPanel({ const [voiceRunId, setVoiceRunId] = useState(null); const [creatingVoiceRun, setCreatingVoiceRun] = useState(false); const [tokenReady, setTokenReady] = useState(false); + const runTestButtonRef = useRef(null); useEffect(() => { let ignore = false; @@ -99,6 +116,13 @@ export function WorkflowTesterPanel({ throw new Error(extractSdkErrorMessage(response.error, "Failed to create browser test run")); } + markActionCompleted("web_call_started"); + markTooltipSeen("web_call"); + posthog.capture(PostHogEvent.WEB_CALL_INITIATED, { + workflow_id: workflowId, + workflow_run_id: response.data.id, + source: "workflow_editor", + }); setVoiceRunId(response.data.id); setActiveMode("audio"); } catch (error) { @@ -106,13 +130,22 @@ export function WorkflowTesterPanel({ } finally { setCreatingVoiceRun(false); } - }, [accessToken, disabled, workflowId]); + }, [accessToken, disabled, markActionCompleted, markTooltipSeen, workflowId]); const authUnavailableReason = tokenReady && !accessToken ? "Authentication is required before testing can start." : null; const effectiveDisabledReason = disabledReason ?? authUnavailableReason; const testerBlocked = disabled || authUnavailableReason !== null; + const showRunTestTooltip = + showWebCallOnboarding && + isVisible && + activeMode === "audio" && + !voiceRunId && + tokenReady && + !!accessToken && + !testerBlocked && + !hasSeenTooltip("web_call"); return (
@@ -145,6 +178,13 @@ export function WorkflowTesterPanel({ ) : null}
+
+

Canvas sync

+ +
@@ -165,6 +205,7 @@ export function WorkflowTesterPanel({ initialContextVariables={initialContextVariables} accessToken={accessToken} onReset={() => setVoiceRunId(null)} + onNodeTransition={onRuntimeNodeTransition} /> ) : ( <> @@ -174,7 +215,11 @@ export function WorkflowTesterPanel({ title="Call this agent in the browser" description="Test the agent over a voice call. Some telephony-only tools, like call transfer, are not yet supported here." action={ - + ); + })} + + ); +} + export function TypingIndicator() { return (
diff --git a/ui/src/app/workflow/[workflowId]/components/workflow-tester/types.ts b/ui/src/app/workflow/[workflowId]/components/workflow-tester/types.ts index 47ddbba..8f59b8d 100644 --- a/ui/src/app/workflow/[workflowId]/components/workflow-tester/types.ts +++ b/ui/src/app/workflow/[workflowId]/components/workflow-tester/types.ts @@ -1,4 +1,5 @@ import type { WorkflowRunTextSessionResponse } from "@/client/types.gen"; +import type { ConversationNodeTransitionItem } from "@/components/workflow/conversation"; export interface TextChatMessage { text: string; @@ -46,6 +47,10 @@ export interface TurnActionState { type: "rewind" | "edit"; } +export type WorkflowRuntimeFocusMode = "pulse" | "follow"; + +export type WorkflowRuntimeNodeTransition = ConversationNodeTransitionItem; + export const EMPTY_TEXT_CHAT_TURNS: TextChatTurn[] = []; export function toTextChatSession(response: WorkflowRunTextSessionResponse): TextChatSession { diff --git a/ui/src/app/workflow/[workflowId]/components/workflow-tester/useTextChatSession.ts b/ui/src/app/workflow/[workflowId]/components/workflow-tester/useTextChatSession.ts index 1797571..14ede77 100644 --- a/ui/src/app/workflow/[workflowId]/components/workflow-tester/useTextChatSession.ts +++ b/ui/src/app/workflow/[workflowId]/components/workflow-tester/useTextChatSession.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { @@ -16,6 +16,7 @@ import { type TextChatTurn, toTextChatSession, type TurnActionState, + type WorkflowRuntimeNodeTransition, } from "./types"; import { extractSdkErrorMessage, getErrorMessage, getReplayCursorTurnId } from "./utils"; @@ -25,6 +26,7 @@ interface UseTextChatSessionProps { initialContextVariables?: Record; disabled: boolean; onActiveChange?: (active: boolean) => void; + onNodeTransition?: (transition: WorkflowRuntimeNodeTransition) => void; } export function useTextChatSession({ @@ -33,6 +35,7 @@ export function useTextChatSession({ initialContextVariables, disabled, onActiveChange, + onNodeTransition, }: UseTextChatSessionProps) { const [session, setSession] = useState(null); const [started, setStarted] = useState(false); @@ -41,6 +44,7 @@ export function useTextChatSession({ const [sendingMessage, setSendingMessage] = useState(false); const [editingTurnId, setEditingTurnId] = useState(null); const [activeTurnAction, setActiveTurnAction] = useState(null); + const lastNotifiedNodeTransitionIdRef = useRef(null); const turns = session?.session_data.turns ?? EMPTY_TEXT_CHAT_TURNS; const editingTurn = editingTurnId @@ -91,6 +95,26 @@ export function useTextChatSession({ onActiveChange?.(started); }, [onActiveChange, started]); + useEffect(() => { + const latestNodeTransition = [...conversationItems] + .reverse() + .find( + (item): item is WorkflowRuntimeNodeTransition => + item.kind === "node-transition" && !!item.nodeId, + ); + + if (!latestNodeTransition?.nodeId) { + return; + } + + if (lastNotifiedNodeTransitionIdRef.current === latestNodeTransition.id) { + return; + } + + lastNotifiedNodeTransitionIdRef.current = latestNodeTransition.id; + onNodeTransition?.(latestNodeTransition); + }, [conversationItems, onNodeTransition]); + useEffect(() => { if (!editingTurnId) { return; diff --git a/ui/src/app/workflow/[workflowId]/page.tsx b/ui/src/app/workflow/[workflowId]/page.tsx index 9726862..bbe36b9 100644 --- a/ui/src/app/workflow/[workflowId]/page.tsx +++ b/ui/src/app/workflow/[workflowId]/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useParams } from 'next/navigation'; +import { useParams, useSearchParams } from 'next/navigation'; import posthog from 'posthog-js'; import { useEffect, useMemo, useState } from 'react'; @@ -18,6 +18,7 @@ import WorkflowLayout from '../WorkflowLayout'; export default function WorkflowDetailPage() { const params = useParams(); + const searchParams = useSearchParams(); const [workflow, setWorkflow] = useState(undefined); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -59,6 +60,7 @@ export default function WorkflowDetailPage() { }, [params.workflowId, user]); const stableUser = useMemo(() => user, [user]); + const openTesterOnLoad = searchParams.get('onboarding') === 'web_call'; if (loading) { return ( @@ -82,6 +84,8 @@ export default function WorkflowDetailPage() { initialWorkflowName={workflow.name} workflowId={workflow.id} workflowUuid={workflow.workflow_uuid ?? undefined} + initialTotalRuns={workflow.total_runs ?? 0} + openTesterOnLoad={openTesterOnLoad} initialFlow={{ nodes: workflow.workflow_definition.nodes as FlowNode[], edges: workflow.workflow_definition.edges as FlowEdge[], diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx index 0253e7b..5121fdf 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/hooks/useWebSocketRTC.tsx @@ -4,7 +4,7 @@ import { client } from "@/client/client.gen"; import { getTurnCredentialsApiV1TurnCredentialsGet, validateUserConfigurationsApiV1UserConfigurationsUserValidateGet, validateWorkflowApiV1WorkflowWorkflowIdValidatePost } from "@/client/sdk.gen"; import { TurnCredentialsResponse } from "@/client/types.gen"; import { WorkflowValidationError } from "@/components/flow/types"; -import type { RealtimeFeedbackMessage as FeedbackMessage } from "@/components/workflow/conversation"; +import type { ConversationNodeTransitionItem, RealtimeFeedbackMessage as FeedbackMessage } from "@/components/workflow/conversation"; import { useAppConfig } from "@/context/AppConfigContext"; import logger from '@/lib/logger'; @@ -16,9 +16,10 @@ interface UseWebSocketRTCProps { workflowRunId: number; accessToken: string | null; initialContextVariables?: Record | null; + onNodeTransition?: (transition: ConversationNodeTransitionItem) => void; } -export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initialContextVariables }: UseWebSocketRTCProps) => { +export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initialContextVariables, onNodeTransition }: UseWebSocketRTCProps) => { const [connectionStatus, setConnectionStatus] = useState<'idle' | 'connecting' | 'connected' | 'failed'>('idle'); const [connectionActive, setConnectionActive] = useState(false); const [isCompleted, setIsCompleted] = useState(false); @@ -53,6 +54,11 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia const pcRef = useRef(null); const wsRef = useRef(null); const timeStartRef = useRef(null); + const onNodeTransitionRef = useRef(onNodeTransition); + + useEffect(() => { + onNodeTransitionRef.current = onNodeTransition; + }, [onNodeTransition]); // Generate a cryptographically secure unique ID const generateSecureId = () => { @@ -394,17 +400,37 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia } case 'rtf-node-transition': { - const { node_name, previous_node_name, allow_interrupt } = message.payload; + const { + node_id, + node_name, + previous_node_id, + previous_node_name, + allow_interrupt, + } = message.payload; currentAllowInterruptRef.current = allow_interrupt; - setFeedbackMessages(prev => [...prev, { + const transitionTimestamp = new Date().toISOString(); + const transition: ConversationNodeTransitionItem = { + kind: 'node-transition', id: `node-${Date.now()}`, + timestamp: transitionTimestamp, + nodeId: node_id, + nodeName: node_name ?? 'Node', + previousNodeId: previous_node_id, + previousNodeName: previous_node_name, + allowInterrupt: allow_interrupt, + }; + setFeedbackMessages(prev => [...prev, { + id: transition.id, type: 'node-transition', - text: node_name, - nodeName: node_name, + text: transition.nodeName, + nodeId: transition.nodeId, + nodeName: transition.nodeName, + previousNodeId: transition.previousNodeId, previousNode: previous_node_name, allowInterrupt: allow_interrupt, - timestamp: new Date().toISOString(), + timestamp: transitionTimestamp, }]); + onNodeTransitionRef.current?.(transition); break; } diff --git a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx index 1087eff..a1e39e4 100644 --- a/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx +++ b/ui/src/app/workflow/[workflowId]/run/[runId]/page.tsx @@ -1,17 +1,14 @@ 'use client'; -import { Check, Copy, ExternalLink, FileText, LoaderCircle, Phone, Video } from 'lucide-react'; +import { Check, Copy, ExternalLink, FileText, Video } from 'lucide-react'; import Link from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; +import { useParams } from 'next/navigation'; import posthog from 'posthog-js'; import { useEffect, useRef, useState } from 'react'; import BrowserCall from '@/app/workflow/[workflowId]/run/[runId]/BrowserCall'; import WorkflowLayout from '@/app/workflow/WorkflowLayout'; -import { - createWorkflowRunApiV1WorkflowWorkflowIdRunsPost, - getWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGet, -} from '@/client/sdk.gen'; +import { getWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGet } from '@/client/sdk.gen'; import { MediaPreviewButton, MediaPreviewDialog } from '@/components/MediaPreviewDialog'; import { OnboardingTooltip } from '@/components/onboarding/OnboardingTooltip'; import { Button } from '@/components/ui/button'; @@ -23,7 +20,6 @@ import { WORKFLOW_RUN_MODES } from '@/constants/workflowRunModes'; import { useOnboarding } from '@/context/OnboardingContext'; import { useAuth } from '@/lib/auth'; import { downloadFile } from '@/lib/files'; -import { getRandomId } from '@/lib/utils'; interface WorkflowRunResponse { mode: string; @@ -151,9 +147,7 @@ function ContextDisplay({ title, context }: { title: string; context: Record(null); const { hasSeenTooltip, markTooltipSeen } = useOnboarding(); @@ -205,24 +199,6 @@ export default function WorkflowRunPage() { fetchWorkflowRun(); }, [params.workflowId, params.runId, auth]); - const handleTestAgain = async () => { - if (startingCall) return; - setStartingCall(true); - try { - const workflowId = Number(params.workflowId); - const workflowRunName = `WR-${getRandomId()}`; - const response = await createWorkflowRunApiV1WorkflowWorkflowIdRunsPost({ - path: { workflow_id: workflowId }, - body: { mode: WORKFLOW_RUN_MODES.SMALL_WEBRTC, name: workflowRunName }, - }); - if (response.data?.id) { - router.push(`/workflow/${workflowId}/run/${response.data.id}`); - } - } finally { - setStartingCall(false); - } - }; - let returnValue = null; const isTextChatRun = workflowRun?.mode === WORKFLOW_RUN_MODES.TEXTCHAT; const showHistoricalRunView = Boolean(workflowRun?.is_completed || isTextChatRun); @@ -271,21 +247,6 @@ export default function WorkflowRunPage() {
- {!isTextChatRun && ( - - )}
@@ -243,7 +214,7 @@ export default function CreateWorkflowPage() { onClick={handleModalContinue} className="w-full" > - Start Web Call + Open and Test Agent diff --git a/ui/src/components/flow/nodes/BaseNode.tsx b/ui/src/components/flow/nodes/BaseNode.tsx index 96847fb..2b3a26c 100644 --- a/ui/src/components/flow/nodes/BaseNode.tsx +++ b/ui/src/components/flow/nodes/BaseNode.tsx @@ -9,8 +9,10 @@ export const BaseNode = forwardRef< invalid?: boolean; selected_through_edge?: boolean; hovered_through_edge?: boolean; + runtimeActive?: boolean; + runtimePulseNonce?: number; } ->(({ className, selected, invalid, selected_through_edge, hovered_through_edge, ...props }, ref) => ( +>(({ children, className, selected, invalid, selected_through_edge, hovered_through_edge, runtimeActive, runtimePulseNonce, ...props }, ref) => (
+ > + {runtimeActive ? ( +
)); BaseNode.displayName = "BaseNode"; diff --git a/ui/src/components/flow/nodes/GenericNode.tsx b/ui/src/components/flow/nodes/GenericNode.tsx index 5b5b1de..824f255 100644 --- a/ui/src/components/flow/nodes/GenericNode.tsx +++ b/ui/src/components/flow/nodes/GenericNode.tsx @@ -608,6 +608,8 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps) invalid={data.invalid} selected_through_edge={data.selected_through_edge} hovered_through_edge={data.hovered_through_edge} + runtimeActive={data.runtime_active} + runtimePulseNonce={data.runtime_pulse_nonce} title={data.name || fallbackTitle} icon={} badgeLabel={badge.label} diff --git a/ui/src/components/flow/nodes/common/NodeContent.tsx b/ui/src/components/flow/nodes/common/NodeContent.tsx index bb5b77e..eaff3d1 100644 --- a/ui/src/components/flow/nodes/common/NodeContent.tsx +++ b/ui/src/components/flow/nodes/common/NodeContent.tsx @@ -10,6 +10,8 @@ interface NodeContentProps { invalid?: boolean; selected_through_edge?: boolean; hovered_through_edge?: boolean; + runtimeActive?: boolean; + runtimePulseNonce?: number; title: string; icon: ReactNode; badgeLabel?: string; @@ -31,6 +33,8 @@ export const NodeContent = ({ invalid, selected_through_edge, hovered_through_edge, + runtimeActive, + runtimePulseNonce, title, icon, badgeLabel, @@ -54,6 +58,8 @@ export const NodeContent = ({ invalid={invalid} selected_through_edge={selected_through_edge} hovered_through_edge={hovered_through_edge} + runtimeActive={runtimeActive} + runtimePulseNonce={runtimePulseNonce} className={`p-0 ${className}`} onDoubleClick={onDoubleClick} > diff --git a/ui/src/components/flow/types.ts b/ui/src/components/flow/types.ts index af7d467..e3a0a0f 100644 --- a/ui/src/components/flow/types.ts +++ b/ui/src/components/flow/types.ts @@ -17,6 +17,8 @@ export type FlowNodeData = { validationMessage?: string | null; selected_through_edge?: boolean; hovered_through_edge?: boolean; + runtime_active?: boolean; + runtime_pulse_nonce?: number; allow_interrupt?: boolean; extraction_enabled?: boolean; extraction_prompt?: string; diff --git a/ui/src/components/workflow/conversation/adapters/fromRealtimeFeedback.ts b/ui/src/components/workflow/conversation/adapters/fromRealtimeFeedback.ts index a4c75ed..dc44f30 100644 --- a/ui/src/components/workflow/conversation/adapters/fromRealtimeFeedback.ts +++ b/ui/src/components/workflow/conversation/adapters/fromRealtimeFeedback.ts @@ -62,7 +62,9 @@ function liveFeedbackItem(message: RealtimeFeedbackMessage, reasoningDurationMs? kind: "node-transition", id: message.id, timestamp: message.timestamp, + nodeId: message.nodeId, nodeName: message.nodeName ?? message.text, + previousNodeId: message.previousNodeId, previousNodeName: message.previousNode, allowInterrupt: message.allowInterrupt, }; @@ -241,7 +243,9 @@ export function conversationItemsFromRealtimeFeedbackEvents(events: RealtimeFeed kind: "node-transition", id: `node-${event.turn}-${index}`, timestamp: event.timestamp, + nodeId: event.payload.node_id, nodeName: event.payload.node_name ?? feedbackEventText(event) ?? "Node", + previousNodeId: event.payload.previous_node_id, previousNodeName: event.payload.previous_node_name ?? event.payload.previous_node, allowInterrupt: event.payload.allow_interrupt, }); diff --git a/ui/src/components/workflow/conversation/adapters/fromTextChatTurns.ts b/ui/src/components/workflow/conversation/adapters/fromTextChatTurns.ts index 3eeb835..8c38762 100644 --- a/ui/src/components/workflow/conversation/adapters/fromTextChatTurns.ts +++ b/ui/src/components/workflow/conversation/adapters/fromTextChatTurns.ts @@ -53,7 +53,9 @@ function conversationItemsFromTextChatEvents( id: `${turnId}-node-${index}`, turnId, timestamp, + nodeId: asString(payload.node_id), nodeName, + previousNodeId: asString(payload.previous_node_id), previousNodeName: asString(payload.previous_node_name), allowInterrupt: typeof payload.allow_interrupt === "boolean" ? payload.allow_interrupt : undefined, }); diff --git a/ui/src/components/workflow/conversation/types.ts b/ui/src/components/workflow/conversation/types.ts index e8f2a01..51e835e 100644 --- a/ui/src/components/workflow/conversation/types.ts +++ b/ui/src/components/workflow/conversation/types.ts @@ -20,7 +20,9 @@ export interface RealtimeFeedbackMessage { arguments?: unknown; result?: unknown; status?: "running" | "completed"; + nodeId?: string; nodeName?: string; + previousNodeId?: string; previousNode?: string; allowInterrupt?: boolean; ttfbSeconds?: number; @@ -40,7 +42,9 @@ export interface RealtimeFeedbackEvent { tool_call_id?: string; arguments?: unknown; result?: unknown; + node_id?: string; node_name?: string; + previous_node_id?: string; previous_node?: string; previous_node_name?: string; allow_interrupt?: boolean; @@ -84,7 +88,9 @@ export interface ConversationToolCallItem extends ConversationItemBase { export interface ConversationNodeTransitionItem extends ConversationItemBase { kind: "node-transition"; + nodeId?: string; nodeName: string; + previousNodeId?: string; previousNodeName?: string; allowInterrupt?: boolean; } diff --git a/ui/src/context/OnboardingContext.tsx b/ui/src/context/OnboardingContext.tsx index d0e5232..df9f793 100644 --- a/ui/src/context/OnboardingContext.tsx +++ b/ui/src/context/OnboardingContext.tsx @@ -2,15 +2,19 @@ import { createContext, useContext, useEffect, useState } from 'react'; -export type TooltipKey = 'web_call' | 'customize_workflow'; // Add more tooltip keys as needed +export type TooltipKey = 'web_call' | 'customize_workflow'; +export type OnboardingActionKey = 'web_call_started'; interface OnboardingState { seenTooltips: TooltipKey[]; + completedActions: OnboardingActionKey[]; } interface OnboardingContextType { hasSeenTooltip: (key: TooltipKey) => boolean; markTooltipSeen: (key: TooltipKey) => void; + hasCompletedAction: (key: OnboardingActionKey) => boolean; + markActionCompleted: (key: OnboardingActionKey) => void; resetOnboarding: () => void; } @@ -18,6 +22,7 @@ const ONBOARDING_STORAGE_KEY = 'dograh_onboarding_state'; const defaultState: OnboardingState = { seenTooltips: [], + completedActions: [], }; const OnboardingContext = createContext(undefined); @@ -59,6 +64,19 @@ export const OnboardingProvider = ({ children }: { children: React.ReactNode }) })); }; + const hasCompletedAction = (key: OnboardingActionKey): boolean => { + return onboardingState.completedActions.includes(key); + }; + + const markActionCompleted = (key: OnboardingActionKey) => { + setOnboardingState(prev => ({ + ...prev, + completedActions: prev.completedActions.includes(key) + ? prev.completedActions + : [...prev.completedActions, key] + })); + }; + const resetOnboarding = () => { setOnboardingState(defaultState); localStorage.removeItem(ONBOARDING_STORAGE_KEY); @@ -69,6 +87,8 @@ export const OnboardingProvider = ({ children }: { children: React.ReactNode }) value={{ hasSeenTooltip, markTooltipSeen, + hasCompletedAction, + markActionCompleted, resetOnboarding }} > diff --git a/ui/src/lib/auth/server.ts b/ui/src/lib/auth/server.ts index 89e883b..c27f595 100644 --- a/ui/src/lib/auth/server.ts +++ b/ui/src/lib/auth/server.ts @@ -131,9 +131,7 @@ export async function getServerAccessToken(): Promise { } } else if (authProvider === 'local') { // Get token from cookies (created by middleware) - const oss_token = await getOSSToken(); - logger.debug(`oss_token: ${oss_token}`); - return oss_token; + return await getOSSToken(); } return null;